From 8940fb75200b21353be29af0f205e8fcf0818ef6 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Wed, 29 Mar 2023 13:34:55 +0200 Subject: [PATCH] [orx-expression-evaluator, orx-keyframer] Split expression evaluator from orx-keyframer --- orx-jvm/orx-expression-evaluator/.gitignore | 2 + orx-jvm/orx-expression-evaluator/README.md | 82 +++++ .../build.gradle.kts} | 37 +-- .../demo/kotlin/DemoExpressionEvaluator01.kt | 17 +- .../demo/kotlin/DemoExpressionEvaluator02.kt | 45 +++ .../src/main/antlr/KeyLangLexer.g4 | 4 - .../src/main/antlr/KeyLangParser.g4 | 4 - .../src/main/kotlin/CompiledFunctions.kt | 124 ++++++++ .../src/main/kotlin/ExpressionDelegate.kt | 40 +++ .../src/main/kotlin/Expressions.kt | 297 +++++++++++------- .../src/test/kotlin/TestCompiledExpression.kt | 33 ++ .../src/test/kotlin/TestCompiledFunctions.kt | 29 ++ .../test/kotlin/TestExpressionDelegates.kt | 16 + .../src/test/kotlin/TestExpressionErrors.kt | 5 +- .../src/test/kotlin/TestExpressions.kt | 12 +- .../src/test/kotlin/TestOperators.kt | 2 +- orx-jvm/orx-keyframer/build.gradle.kts | 21 ++ .../src/main/kotlin/Keyframer.kt | 110 ++++--- .../src/test/kotlin/TestKeyframerErrors.kt | 3 +- settings.gradle.kts | 1 + 20 files changed, 685 insertions(+), 199 deletions(-) create mode 100644 orx-jvm/orx-expression-evaluator/.gitignore create mode 100644 orx-jvm/orx-expression-evaluator/README.md rename orx-jvm/{orx-keyframer/build.gradle => orx-expression-evaluator/build.gradle.kts} (59%) rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/demo/kotlin/DemoExpressionEvaluator01.kt (65%) create mode 100644 orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator02.kt rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/main/antlr/KeyLangLexer.g4 (95%) rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/main/antlr/KeyLangParser.g4 (97%) create mode 100644 orx-jvm/orx-expression-evaluator/src/main/kotlin/CompiledFunctions.kt create mode 100644 orx-jvm/orx-expression-evaluator/src/main/kotlin/ExpressionDelegate.kt rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/main/kotlin/Expressions.kt (50%) create mode 100644 orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledExpression.kt create mode 100644 orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledFunctions.kt create mode 100644 orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionDelegates.kt rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/test/kotlin/TestExpressionErrors.kt (94%) rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/test/kotlin/TestExpressions.kt (93%) rename orx-jvm/{orx-keyframer => orx-expression-evaluator}/src/test/kotlin/TestOperators.kt (95%) create mode 100644 orx-jvm/orx-keyframer/build.gradle.kts diff --git a/orx-jvm/orx-expression-evaluator/.gitignore b/orx-jvm/orx-expression-evaluator/.gitignore new file mode 100644 index 00000000..f0dd3232 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/.gitignore @@ -0,0 +1,2 @@ +*.tokens +gen/ \ No newline at end of file diff --git a/orx-jvm/orx-expression-evaluator/README.md b/orx-jvm/orx-expression-evaluator/README.md new file mode 100644 index 00000000..b0297a3d --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/README.md @@ -0,0 +1,82 @@ +# orx-expression-evaluator + +Tools to evaluate expression strings + +# Expression evaluator + +```kotlin +val expression = "x + y" +val constants = mapOf("x" to 1.0, "y" to 2.0) +evaluateExpression(expression, constants) +``` +## Built-in expression functions + +Unary functions: + * `abs(x)` + * `acos(x)` + * `asin(x)` + * `atan(x)` + * `ceil(x)` + * `cos(x)` + * `degrees(x)` + * `exps(x)` + * `floor(x)` + * `radians(x)` + * `round(x)` + * `saturate(x)`, clamp x to [0.0, 1.0] + * `sqrt(x)` + * `tan(x)` + +Binary functions: + * `atan2(x, y)` + * `length(x, y)`, the Euclidean length of the vector (x,y) + * `max(x, y)`, + * `min(x, y)`, + * `pow(x, n)` + * `random(x, y)`, return a random number in [x, y) + +Ternary functions: + * `length(x, y, z)`, the Euclidean length of the vector (x, y, z) + * `max(x, y, z)` + * `min(x, y, z)` + * `mix(l, r, f)` + * `smoothstep(e0, e1, x)` + * `sum(x, y, z)` + +Quaternary functions: +* `length(x, y, z, w)`, the Euclidean length of the vector (x, y, z) +* `max(a, b, c, d)` +* `min(a, b, c, d)` +* `sum(a, b, c, d)` + +Quinary functions: +* `map(x0, x1, y0, y1, v)` +* `max(a, b, c, d, e)` +* `min(a, b, c, d, e)` +* `sum(a, b, c, d, e)` + +# Compiled functions + +```kotlin +val expression = "x * 5.0 + cos(x)" +val f = compileFunction1(expression, "x") +f(0.0) +``` + +```kotlin +val expression = "x * 5.0 + cos(x) * y" +val f = compileFunction2(expression, "x", "y") +f(0.0, 0.4) +``` + +# Property delegates + +```kotlin +val constants = mutableMapOf("width" to 300.0) +val settings = object { + var xExpression = "cos(t) * 50.0 + width / 2.0" +} +val xFunction by watchingExpression1(settings::xExpression, "t", constants) + +xFunction(1.0) +``` diff --git a/orx-jvm/orx-keyframer/build.gradle b/orx-jvm/orx-expression-evaluator/build.gradle.kts similarity index 59% rename from orx-jvm/orx-keyframer/build.gradle rename to orx-jvm/orx-expression-evaluator/build.gradle.kts index 722f30ad..5f32fb36 100644 --- a/orx-jvm/orx-keyframer/build.gradle +++ b/orx-jvm/orx-expression-evaluator/build.gradle.kts @@ -1,6 +1,14 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { - id("org.openrndr.extra.convention.kotlin-jvm") - id("antlr") + org.openrndr.extra.convention.`kotlin-jvm` + antlr +} + +tasks.generateGrammarSource { + maxHeapSize = "64m" + arguments.addAll(listOf("-visitor", "-long-messages", "-package", "org.openrndr.extra.expressions.antlr")) + outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/expressions/antlr") } sourceSets { @@ -11,36 +19,23 @@ sourceSets { } } -tasks.test { - useJUnitPlatform { - includeEngines("spek2") - } -} - -generateGrammarSource { - maxHeapSize = "64m" - arguments += ["-visitor", "-long-messages"] - outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString()) +tasks.withType { + kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") } dependencies { antlr(libs.antlr.core) implementation(libs.antlr.runtime) - implementation(project(":orx-noise")) - implementation(project(":orx-easing")) implementation(libs.openrndr.application) implementation(libs.openrndr.math) - implementation(libs.gson) - implementation(libs.kotlin.reflect) + implementation(libs.kotlin.coroutines) + implementation(project(":orx-property-watchers")) + implementation(project(":orx-noise")) testImplementation(libs.kluent) - testImplementation(libs.spek.dsl) - testRuntimeOnly(libs.spek.junit5) - testRuntimeOnly(libs.kotlin.reflect) - demoImplementation(project(":orx-jvm:orx-panel")) demoImplementation(project(":orx-jvm:orx-gui")) } tasks.getByName("compileKotlin").dependsOn("generateGrammarSource") tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource") tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource") -tasks.getByName("sourcesJar").dependsOn("generateGrammarSource") \ No newline at end of file +tasks.getByName("sourcesJar").dependsOn("generateGrammarSource") diff --git a/orx-jvm/orx-keyframer/src/demo/kotlin/DemoExpressionEvaluator01.kt b/orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator01.kt similarity index 65% rename from orx-jvm/orx-keyframer/src/demo/kotlin/DemoExpressionEvaluator01.kt rename to orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator01.kt index 97be117b..5e89959f 100644 --- a/orx-jvm/orx-keyframer/src/demo/kotlin/DemoExpressionEvaluator01.kt +++ b/orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator01.kt @@ -1,7 +1,7 @@ import org.openrndr.application +import org.openrndr.extra.expressions.evaluateExpression import org.openrndr.extra.gui.GUI import org.openrndr.extra.gui.addTo -import org.openrndr.extra.keyframer.evaluateExpression import org.openrndr.extra.parameters.TextParameter fun main() { @@ -13,9 +13,11 @@ fun main() { val settings = object { @TextParameter("x expression", order = 10) var xExpression = "cos(t) * 50.0 + width / 2.0" + @TextParameter("y expression", order = 20) var yExpression = "sin(t) * 50.0 + height / 2.0" - @TextParameter("radius expression", order = 30 ) + + @TextParameter("radius expression", order = 30) var radiusExpression = "cos(t) * 50.0 + 50.0" }.addTo(gui) @@ -23,10 +25,15 @@ fun main() { extend { //gui.visible = mouse.position.x < 200.0 - val expressionContext = mapOf("t" to seconds, "width" to drawer.bounds.width, "height" to drawer.bounds.height) + val expressionContext = + mapOf("t" to seconds, "width" to drawer.bounds.width, "height" to drawer.bounds.height) - fun eval(expression: String) : Double = - try { evaluateExpression(expression, expressionContext) ?: 0.0 } catch (e: Throwable) { 0.0 } + fun eval(expression: String): Double = + try { + evaluateExpression(expression, expressionContext) ?: 0.0 + } catch (e: Throwable) { + 0.0 + } val x = eval(settings.xExpression) val y = eval(settings.yExpression) diff --git a/orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator02.kt b/orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator02.kt new file mode 100644 index 00000000..99506085 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/demo/kotlin/DemoExpressionEvaluator02.kt @@ -0,0 +1,45 @@ +import org.openrndr.application +import org.openrndr.extra.expressions.evaluateExpression +import org.openrndr.extra.expressions.watchingExpression1 +import org.openrndr.extra.gui.GUI +import org.openrndr.extra.gui.addTo +import org.openrndr.extra.parameters.TextParameter + +/** + * Improved version of DemoExpressionEvaluator01, it uses [watchingExpression1] to automatically convert an expression + * string into a function with a parameter "t". + */ +fun main() { + application { + program { + val gui = GUI() + gui.compartmentsCollapsedByDefault = false + + // the constants used in our expressions + val constants = mutableMapOf("width" to drawer.width.toDouble(), "height" to drawer.height.toDouble()) + + val settings = object { + @TextParameter("x expression", order = 10) + var xExpression = "cos(t) * 50.0 + width / 2.0" + + @TextParameter("y expression", order = 20) + var yExpression = "sin(t) * 50.0 + height / 2.0" + + @TextParameter("radius expression", order = 30) + var radiusExpression = "cos(t) * 50.0 + 50.0" + }.addTo(gui) + + val xFunction by watchingExpression1(settings::xExpression, "t", constants) + val yFunction by watchingExpression1(settings::yExpression, "t", constants) + val radiusFunction by watchingExpression1(settings::radiusExpression, "t", constants) + + extend(gui) + extend { + val x = xFunction(seconds) + val y = yFunction(seconds) + val radius = radiusFunction(seconds) + drawer.circle(x, y, radius) + } + } + } +} \ No newline at end of file diff --git a/orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4 b/orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangLexer.g4 similarity index 95% rename from orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4 rename to orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangLexer.g4 index f9fa14cc..e56588cb 100644 --- a/orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4 +++ b/orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangLexer.g4 @@ -1,9 +1,5 @@ lexer grammar KeyLangLexer; -@header { -package org.openrndr.extra.keyframer.antlr; -} - channels { WHITESPACE } // Whitespace diff --git a/orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4 b/orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangParser.g4 similarity index 97% rename from orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4 rename to orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangParser.g4 index 3f02a1fb..166bbf5a 100644 --- a/orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4 +++ b/orx-jvm/orx-expression-evaluator/src/main/antlr/KeyLangParser.g4 @@ -1,10 +1,6 @@ parser grammar KeyLangParser; -@header { -package org.openrndr.extra.keyframer.antlr; -} - options { tokenVocab=KeyLangLexer; } keyLangFile : lines=line+ ; diff --git a/orx-jvm/orx-expression-evaluator/src/main/kotlin/CompiledFunctions.kt b/orx-jvm/orx-expression-evaluator/src/main/kotlin/CompiledFunctions.kt new file mode 100644 index 00000000..fd482d18 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/main/kotlin/CompiledFunctions.kt @@ -0,0 +1,124 @@ +package org.openrndr.extra.expressions + +import org.antlr.v4.runtime.tree.ParseTreeWalker + +/** + * Compile a (Double)->Double function from an expression string + * @param expression the expression string to be compiled + * @param parameter0 the name of the first parameter + * @param constants a map of named constant values that can be referred from the expression + * @param functions a map of named functions that can be invoked from the expression + * @param error in case the expression fails to compile or evaluate, this function is invoked instead + */ +fun compileFunction1( + expression: String, + parameter0: String, + constants: Map = mapOf(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (Double) -> Double = { 0.0 }, +): (Double) -> Double { + require(!constants.containsKey(parameter0)) + try { + val root = expressionRoot(expression) + val variables = mutableMapOf() + variables.putAll(constants) + val listener = ExpressionListener(functions, variables) + + return { p0 -> + variables[parameter0] = p0 + try { + ParseTreeWalker.DEFAULT.walk(listener, root) + listener.lastExpressionResult ?: error("no result") + } catch (e: ExpressionException) { + error(p0) + } + } + } catch (e: ExpressionException) { + return error + } +} + +/** + * Compile a (Double, Double)->Double function from an expression string + * @param expression the expression string to be compiled + * @param parameter0 the name of the first parameter + * @param parameter1 the name of the second parameter + * @param constants a map of named constant values that can be referred from the expression + * @param functions a map of named functions that can be invoked from the expression + * @param error in case the expression fails to compile or evaluate, this function is invoked instead + */ +fun compileFunction2( + expression: String, + parameter0: String, + parameter1: String, + constants: Map = mapOf(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (p0: Double, p1: Double) -> Double = { _, _ -> 0.0 }, +): (p0: Double, p1: Double) -> Double { + require(!constants.containsKey(parameter0)) + require(!constants.containsKey(parameter1)) + try { + val root = expressionRoot(expression) + val variables = mutableMapOf() + variables.putAll(constants) + val listener = ExpressionListener(functions, variables) + + return { p0, p1 -> + variables[parameter0] = p0 + variables[parameter1] = p1 + try { + ParseTreeWalker.DEFAULT.walk(listener, root) + listener.lastExpressionResult ?: error("no result") + } catch (e: ExpressionException) { + error(p0, p1) + } + } + } catch (e: ExpressionException) { + return error + } +} + +/** + * Compile a (Double, Double, Double)->Double function from an expression string + * @param expression the expression string to be compiled + * @param parameter0 the name of the first parameter + * @param parameter1 the name of the second parameter + * @param parameter2 the name of the third parameter + * @param constants a map of named constant values that can be referred from the expression + * @param functions a map of named functions that can be invoked from the expression + * @param error in case the expression fails to compile or evaluate, this function is invoked instead + */ +fun compileFunction3( + expression: String, + parameter0: String, + parameter1: String, + parameter2: String, + constants: Map = mapOf(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (p0: Double, p1: Double, p2: Double) -> Double = { _, _, _ -> 0.0 } +): (p0: Double, p1: Double, p2: Double) -> Double { + require(!constants.containsKey(parameter0)) + require(!constants.containsKey(parameter1)) + require(!constants.containsKey(parameter2)) + + try { + val root = expressionRoot(expression) + val variables = mutableMapOf() + variables.putAll(constants) + val listener = ExpressionListener(functions, variables) + + return { p0, p1, p2 -> + variables[parameter0] = p0 + variables[parameter1] = p1 + variables[parameter2] = p2 + try { + ParseTreeWalker.DEFAULT.walk(listener, root) + listener.lastExpressionResult ?: error("no result") + } catch (e: ExpressionException) { + error(p0, p1, p2) + } + } + } catch(e: ExpressionException) { + return error + } +} diff --git a/orx-jvm/orx-expression-evaluator/src/main/kotlin/ExpressionDelegate.kt b/orx-jvm/orx-expression-evaluator/src/main/kotlin/ExpressionDelegate.kt new file mode 100644 index 00000000..0b2ced2a --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/main/kotlin/ExpressionDelegate.kt @@ -0,0 +1,40 @@ +package org.openrndr.extra.expressions + +import org.openrndr.extra.propertywatchers.watchingProperty +import kotlin.reflect.KProperty0 + +fun watchingExpression1( + expressionProperty: KProperty0, + parameter0: String = "x", + constants: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (p0: Double) -> Double = { 0.0 } +) = + watchingProperty(expressionProperty) { + compileFunction1(it, parameter0, constants, functions, error) + } + +fun watchingExpression2( + expressionProperty: KProperty0, + parameter0: String = "x", + parameter1: String = "y", + constants: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (p0: Double, p1: Double) -> Double = { _, _ -> 0.0 } +) = + watchingProperty(expressionProperty) { + compileFunction2(it, parameter0, parameter1, constants, functions, error) + } + +fun watchingExpression3( + expressionProperty: KProperty0, + parameter0: String = "x", + parameter1: String = "y", + parameter2: String = "z", + constants: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY, + error: (p0: Double, p1: Double, p2: Double) -> Double = { _, _, _ -> 0.0 } +) = + watchingProperty(expressionProperty) { + compileFunction3(it, parameter0, parameter1, parameter2, constants, functions, error) + } \ No newline at end of file diff --git a/orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt b/orx-jvm/orx-expression-evaluator/src/main/kotlin/Expressions.kt similarity index 50% rename from orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt rename to orx-jvm/orx-expression-evaluator/src/main/kotlin/Expressions.kt index 6a40ca95..778780d6 100644 --- a/orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt +++ b/orx-jvm/orx-expression-evaluator/src/main/kotlin/Expressions.kt @@ -1,11 +1,11 @@ -package org.openrndr.extra.keyframer +package org.openrndr.extra.expressions import org.antlr.v4.runtime.* import org.antlr.v4.runtime.tree.ParseTreeWalker import org.antlr.v4.runtime.tree.TerminalNode -import org.openrndr.extra.keyframer.antlr.KeyLangLexer -import org.openrndr.extra.keyframer.antlr.KeyLangParser -import org.openrndr.extra.keyframer.antlr.KeyLangParserBaseListener +import org.openrndr.extra.expressions.antlr.KeyLangLexer +import org.openrndr.extra.expressions.antlr.KeyLangParser +import org.openrndr.extra.expressions.antlr.KeyLangParserBaseListener import org.openrndr.extra.noise.uniform import org.openrndr.math.* import java.util.* @@ -19,12 +19,12 @@ typealias Function4 = (Double, Double, Double, Double) -> Double typealias Function5 = (Double, Double, Double, Double, Double) -> Double class FunctionExtensions( - val functions0: Map = emptyMap(), - val functions1: Map = emptyMap(), - val functions2: Map = emptyMap(), - val functions3: Map = emptyMap(), - val functions4: Map = emptyMap(), - val functions5: Map = emptyMap() + val functions0: Map = emptyMap(), + val functions1: Map = emptyMap(), + val functions2: Map = emptyMap(), + val functions3: Map = emptyMap(), + val functions4: Map = emptyMap(), + val functions5: Map = emptyMap() ) { companion object { val EMPTY = FunctionExtensions() @@ -41,11 +41,14 @@ internal enum class IDType { FUNCTION5 } -internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) : - KeyLangParserBaseListener() { +internal class ExpressionListener( + val functions: FunctionExtensions = FunctionExtensions.EMPTY, + val constants: Map = mapOf() +) : + KeyLangParserBaseListener() { val doubleStack = Stack() val functionStack = Stack<(DoubleArray) -> Double>() - val variables = mutableMapOf() + val idTypeStack = Stack() var lastExpressionResult: Double? = null @@ -61,10 +64,10 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx lastExpressionResult = result } - override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) { - val value = doubleStack.pop() - variables[ctx.ID()?.text ?: error("buh")] = value - } +// override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) { +// val value = doubleStack.pop() +// variables[ctx.ID()?.text ?: error("buh")] = value +// } override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) { val op = doubleStack.pop() @@ -248,114 +251,145 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx doubleStack.push(node.text.toDouble()) } if (type == KeyLangParser.ID) { - val name = node.text.replace("`","") + val name = node.text.replace("`", "") @Suppress("DIVISION_BY_ZERO") when (val idType = idTypeStack.pop()) { IDType.VARIABLE -> doubleStack.push( - when (name) { - "PI" -> PI - else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0) - } + when (name) { + "PI" -> PI + else -> constants[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0) + } ) IDType.FUNCTION0 -> { val function: (DoubleArray) -> Double = - when (name) { - "random" -> { _ -> Double.uniform(0.0, 1.0) } - else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } } - ?: errorValue( - "unresolved function: '${name}()'" - ) { _ -> error("this is the error function") } - } + when (name) { + "random" -> { _ -> Double.uniform(0.0, 1.0) } + else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } } + ?: errorValue( + "unresolved function: '${name}()'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } IDType.FUNCTION1 -> { val function: (DoubleArray) -> Double = - when (name) { - "sqrt" -> { x -> sqrt(x[0]) } - "radians" -> { x -> Math.toRadians(x[0]) } - "degrees" -> { x -> Math.toDegrees(x[0]) } - "cos" -> { x -> cos(x[0]) } - "sin" -> { x -> sin(x[0]) } - "tan" -> { x -> tan(x[0]) } - "atan" -> { x -> atan(x[0]) } - "acos" -> { x -> acos(x[0]) } - "asin" -> { x -> asin(x[0]) } - "exp" -> { x -> exp(x[0]) } - "abs" -> { x -> abs(x[0]) } - "floor" -> { x -> floor(x[0]) } - "round" -> { x -> round(x[0]) } - "ceil" -> { x -> ceil(x[0]) } - "saturate" -> { x -> x[0].coerceIn(0.0, 1.0) } - else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } } - ?: errorValue( - "unresolved function: '${name}(x0)'" - ) { _ -> error("this is the error function") } - } + when (name) { + "sqrt" -> { x -> sqrt(x[0]) } + "radians" -> { x -> Math.toRadians(x[0]) } + "degrees" -> { x -> Math.toDegrees(x[0]) } + "cos" -> { x -> cos(x[0]) } + "sin" -> { x -> sin(x[0]) } + "tan" -> { x -> tan(x[0]) } + "atan" -> { x -> atan(x[0]) } + "acos" -> { x -> acos(x[0]) } + "asin" -> { x -> asin(x[0]) } + "exp" -> { x -> exp(x[0]) } + "abs" -> { x -> abs(x[0]) } + "floor" -> { x -> floor(x[0]) } + "round" -> { x -> round(x[0]) } + "ceil" -> { x -> ceil(x[0]) } + "saturate" -> { x -> x[0].coerceIn(0.0, 1.0) } + else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } } + ?: errorValue( + "unresolved function: '${name}(x0)'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } + IDType.FUNCTION2 -> { val function: (DoubleArray) -> Double = - when (name) { - "max" -> { x -> max(x[0], x[1]) } - "min" -> { x -> min(x[0], x[1]) } - "pow" -> { x -> x[0].pow(x[1]) } - "mod" -> { x -> x[0].mod(x[1]) } - "atan2" -> { x -> atan2(x[0], x[1]) } - "random" -> { x -> Double.uniform(x[0], x[1]) } - "length" -> { x -> Vector2(x[0], x[1]).length } - else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } } - ?: errorValue( - "unresolved function: '${name}(x0, x1)'" - ) { _ -> error("this is the error function") } - } + when (name) { + "max" -> { x -> max(x[0], x[1]) } + "min" -> { x -> min(x[0], x[1]) } + "pow" -> { x -> x[0].pow(x[1]) } + "mod" -> { x -> x[0].mod(x[1]) } + "atan2" -> { x -> atan2(x[0], x[1]) } + "random" -> { x -> Double.uniform(x[0], x[1]) } + "length" -> { x -> Vector2(x[0], x[1]).length } + else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } } + ?: errorValue( + "unresolved function: '${name}(x0, x1)'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } + IDType.FUNCTION3 -> { val function: (DoubleArray) -> Double = - when (name) { - "mix" -> { x -> mix(x[0], x[1], x[2]) } - "min" -> { x -> x.minOrNull()!! } - "max" -> { x -> x.maxOrNull()!! } - "sum" -> { x -> x.sum() } - "smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) } - "length" -> { x -> Vector3(x[0], x[1], x[2]).length } - else -> functions.functions3[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2]) } } - ?: errorValue( - "unresolved function: '${name}(x0, x1, x2)'" - ) { _ -> error("this is the error function") } + when (name) { + "mix" -> { x -> mix(x[0], x[1], x[2]) } + "min" -> { x -> x.minOrNull()!! } + "max" -> { x -> x.maxOrNull()!! } + "sum" -> { x -> x.sum() } + "smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) } + "length" -> { x -> Vector3(x[0], x[1], x[2]).length } + else -> functions.functions3[name]?.let { + { x: DoubleArray -> + it.invoke( + x[0], + x[1], + x[2] + ) + } } + ?: errorValue( + "unresolved function: '${name}(x0, x1, x2)'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } + IDType.FUNCTION4 -> { val function: (DoubleArray) -> Double = - when (name) { - "min" -> { x -> x.minOrNull()!! } - "max" -> { x -> x.maxOrNull()!! } - "sum" -> { x -> x.sum() } - else -> functions.functions4[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } } - ?: errorValue( - "unresolved function: '${name}(x0, x1, x2, x3)'" - ) { _ -> error("this is the error function") } + when (name) { + "min" -> { x -> x.minOrNull()!! } + "max" -> { x -> x.maxOrNull()!! } + "sum" -> { x -> x.sum() } + else -> functions.functions4[name]?.let { + { x: DoubleArray -> + it.invoke( + x[0], + x[1], + x[2], + x[3] + ) + } } + ?: errorValue( + "unresolved function: '${name}(x0, x1, x2, x3)'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } IDType.FUNCTION5 -> { val function: (DoubleArray) -> Double = - when (name) { - "min" -> { x -> x.minOrNull()!! } - "max" -> { x -> x.maxOrNull()!! } - "sum" -> { x -> x.sum() } - "map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) } - else -> functions.functions5[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } } - ?: errorValue( - "unresolved function: '${name}(x0, x1, x2, x3, x4)'" - ) { _ -> error("this is the error function") } + when (name) { + "min" -> { x -> x.minOrNull()!! } + "max" -> { x -> x.maxOrNull()!! } + "sum" -> { x -> x.sum() } + "map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) } + else -> functions.functions5[name]?.let { + { x: DoubleArray -> + it.invoke( + x[0], + x[1], + x[2], + x[3], + x[4] + ) + } } + ?: errorValue( + "unresolved function: '${name}(x0, x1, x2, x3, x4)'" + ) { _ -> error("this is the error function") } + } functionStack.push(function) } + else -> error("unsupported id-type $idType") } } @@ -365,33 +399,86 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx class ExpressionException(message: String) : RuntimeException(message) fun evaluateExpression( - input: String, - variables: Map = emptyMap(), - functions: FunctionExtensions = FunctionExtensions.EMPTY + expression: String, + constants: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY ): Double? { - val lexer = KeyLangLexer(CharStreams.fromString(input)) + val lexer = KeyLangLexer(CharStreams.fromString(expression)) val parser = KeyLangParser(CommonTokenStream(lexer)) parser.removeErrorListeners() parser.addErrorListener(object : BaseErrorListener() { override fun syntaxError( - recognizer: Recognizer<*, *>?, - offendingSymbol: Any?, - line: Int, - charPositionInLine: Int, - msg: String?, - e: RecognitionException? + recognizer: Recognizer<*, *>?, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String?, + e: RecognitionException? ) { - throw ExpressionException("parser error in expression: '$input'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]") + throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]") } }) val root = parser.keyLangFile() - val listener = ExpressionListener(functions) - listener.variables.putAll(variables) + val listener = ExpressionListener(functions, constants) try { ParseTreeWalker.DEFAULT.walk(listener, root) } catch (e: ExpressionException) { throw ExpressionException(e.message ?: "") } return listener.lastExpressionResult -} \ No newline at end of file +} + +fun compileExpression( + expression: String, + constants: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY +): () -> Double { + val lexer = KeyLangLexer(CharStreams.fromString(expression)) + val parser = KeyLangParser(CommonTokenStream(lexer)) + parser.removeErrorListeners() + parser.addErrorListener(object : BaseErrorListener() { + override fun syntaxError( + recognizer: Recognizer<*, *>?, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String?, + e: RecognitionException? + ) { + throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]") + } + }) + val root = parser.keyLangFile() + val listener = ExpressionListener(functions, constants) + + + return { + try { + ParseTreeWalker.DEFAULT.walk(listener, root) + } catch (e: ExpressionException) { + throw ExpressionException(e.message ?: "") + } + listener.lastExpressionResult ?: error("no result") + } +} + +internal fun expressionRoot(expression: String): KeyLangParser.KeyLangFileContext { + val lexer = KeyLangLexer(CharStreams.fromString(expression)) + val parser = KeyLangParser(CommonTokenStream(lexer)) + parser.removeErrorListeners() + parser.addErrorListener(object : BaseErrorListener() { + override fun syntaxError( + recognizer: Recognizer<*, *>?, + offendingSymbol: Any?, + line: Int, + charPositionInLine: Int, + msg: String?, + e: RecognitionException? + ) { + throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]") + } + }) + return parser.keyLangFile() +} + diff --git a/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledExpression.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledExpression.kt new file mode 100644 index 00000000..61fbc482 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledExpression.kt @@ -0,0 +1,33 @@ +import org.amshove.kluent.invoking +import org.amshove.kluent.`should throw` +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.openrndr.extra.expressions.ExpressionException +import org.openrndr.extra.expressions.compileExpression + +class TestCompiledExpression { + @Test + fun `a simple compiled expression`() { + val expression = "someValue" + val function = compileExpression(expression, constants = mutableMapOf("someValue" to 5.0)) + function().shouldBeEqualTo(5.0) + } + + @Test + fun `a compiled expression with updated context`() { + val expression = "someValue" + val context = mutableMapOf("someValue" to 5.0) + val function = compileExpression(expression, constants = context) + function().shouldBeEqualTo(5.0) + context["someValue"] = 6.0 + function().shouldBeEqualTo(6.0) + } + + @Test + fun `an erroneous compiled expression`() { + val expression = "1bork" + invoking { + compileExpression(expression, constants = mutableMapOf("someValue" to 5.0)) + } `should throw` ExpressionException::class + } +} \ No newline at end of file diff --git a/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledFunctions.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledFunctions.kt new file mode 100644 index 00000000..45195cb3 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestCompiledFunctions.kt @@ -0,0 +1,29 @@ +import org.amshove.kluent.invoking +import org.amshove.kluent.`should throw` +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.openrndr.extra.expressions.* + +class TestCompiledFunctions { + @Test + fun `a simple compiled function1`() { + val expression = "t" + val function = compileFunction1(expression, "t") + function(-5.0).shouldBeEqualTo(-5.0) + function(5.0).shouldBeEqualTo(5.0) + } + + @Test + fun `a simple compiled function2`() { + val expression = "x + y" + val function = compileFunction2(expression, "x", "y") + function(1.0, 2.0).shouldBeEqualTo(3.0) + } + + @Test + fun `a simple compiled function3`() { + val expression = "x + y + z" + val function = compileFunction3(expression, "x", "y", "z") + function(1.0, 2.0, 3.0).shouldBeEqualTo(6.0) + } +} \ No newline at end of file diff --git a/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionDelegates.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionDelegates.kt new file mode 100644 index 00000000..8f811bd7 --- /dev/null +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionDelegates.kt @@ -0,0 +1,16 @@ +import org.amshove.kluent.shouldBeEqualTo +import org.openrndr.extra.expressions.watchingExpression1 +import kotlin.test.Test + +class TestExpressionDelegates { + + @Test + fun test() { + val state = object { + var expression = "x * x" + val function1 by watchingExpression1(::expression, "x") + } + state.function1(5.0).shouldBeEqualTo(25.0) + } + +} \ No newline at end of file diff --git a/orx-jvm/orx-keyframer/src/test/kotlin/TestExpressionErrors.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionErrors.kt similarity index 94% rename from orx-jvm/orx-keyframer/src/test/kotlin/TestExpressionErrors.kt rename to orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionErrors.kt index 07e5817c..0367377c 100644 --- a/orx-jvm/orx-keyframer/src/test/kotlin/TestExpressionErrors.kt +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressionErrors.kt @@ -1,8 +1,9 @@ import org.amshove.kluent.`should throw` import org.amshove.kluent.`with message` import org.amshove.kluent.invoking -import org.openrndr.extra.keyframer.ExpressionException -import org.openrndr.extra.keyframer.evaluateExpression +import org.openrndr.extra.expressions.ExpressionException +import org.openrndr.extra.expressions.evaluateExpression + import kotlin.test.Test class TestExpressionErrors { diff --git a/orx-jvm/orx-keyframer/src/test/kotlin/TestExpressions.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressions.kt similarity index 93% rename from orx-jvm/orx-keyframer/src/test/kotlin/TestExpressions.kt rename to orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressions.kt index 06914e02..c322a857 100644 --- a/orx-jvm/orx-keyframer/src/test/kotlin/TestExpressions.kt +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestExpressions.kt @@ -1,25 +1,21 @@ -import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeNear -import org.openrndr.extra.keyframer.FunctionExtensions -import org.openrndr.extra.keyframer.evaluateExpression -import org.openrndr.math.map +import org.openrndr.extra.expressions.FunctionExtensions +import org.openrndr.extra.expressions.evaluateExpression import kotlin.test.Test class TestExpressions { - - @Test fun `a value reference`() { val expression = "someValue" - val result = evaluateExpression(expression, variables= mapOf("someValue" to 5.0)) + val result = evaluateExpression(expression, constants= mapOf("someValue" to 5.0)) result?.shouldBeEqualTo(5.0) } @Test fun `a backticked value reference`() { val expression = "`some-value`" - val result = evaluateExpression(expression, variables= mapOf("some-value" to 5.0)) + val result = evaluateExpression(expression, constants= mapOf("some-value" to 5.0)) result?.shouldBeEqualTo(5.0) } diff --git a/orx-jvm/orx-keyframer/src/test/kotlin/TestOperators.kt b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestOperators.kt similarity index 95% rename from orx-jvm/orx-keyframer/src/test/kotlin/TestOperators.kt rename to orx-jvm/orx-expression-evaluator/src/test/kotlin/TestOperators.kt index 90e7ec80..a3191252 100644 --- a/orx-jvm/orx-keyframer/src/test/kotlin/TestOperators.kt +++ b/orx-jvm/orx-expression-evaluator/src/test/kotlin/TestOperators.kt @@ -1,5 +1,5 @@ import org.amshove.kluent.shouldBeNear -import org.openrndr.extra.keyframer.evaluateExpression +import org.openrndr.extra.expressions.evaluateExpression import kotlin.test.Test class TestOperators { diff --git a/orx-jvm/orx-keyframer/build.gradle.kts b/orx-jvm/orx-keyframer/build.gradle.kts new file mode 100644 index 00000000..64e87c34 --- /dev/null +++ b/orx-jvm/orx-keyframer/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + org.openrndr.extra.convention.`kotlin-jvm` +} + +tasks.withType { + kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") +} + +dependencies { + implementation(libs.openrndr.application) + implementation(libs.openrndr.math) + implementation(libs.gson) + implementation(libs.kotlin.reflect) + implementation(project(":orx-noise")) + implementation(project(":orx-easing")) + implementation(project(":orx-jvm:orx-expression-evaluator")) + demoImplementation(project(":orx-jvm:orx-panel")) + testImplementation(libs.kluent) +} diff --git a/orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt b/orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt index 1a71d5da..541e10a7 100644 --- a/orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt +++ b/orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt @@ -6,6 +6,9 @@ import com.google.gson.reflect.TypeToken import org.openrndr.color.ColorRGBa import org.openrndr.extra.easing.Easing import org.openrndr.extra.easing.EasingFunction +import org.openrndr.extra.expressions.ExpressionException +import org.openrndr.extra.expressions.FunctionExtensions +import org.openrndr.extra.expressions.evaluateExpression import org.openrndr.math.Vector2 import org.openrndr.math.Vector3 import org.openrndr.math.Vector4 @@ -64,41 +67,41 @@ open class Keyframer { inner class DoubleChannel(key: String, defaultValue: Double = 0.0) : - CompoundChannel(arrayOf(key), arrayOf(defaultValue)) { + CompoundChannel(arrayOf(key), arrayOf(defaultValue)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0) } inner class Vector2Channel(keys: Array, defaultValue: Vector2 = Vector2.ZERO) : - CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) { + CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1)) } inner class Vector3Channel(keys: Array, defaultValue: Vector3 = Vector3.ZERO) : - CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) { + CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 = - Vector3(getValue(0), getValue(1), getValue(2)) + Vector3(getValue(0), getValue(1), getValue(2)) } inner class Vector4Channel(keys: Array, defaultValue: Vector4 = Vector4.ZERO) : - CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) { + CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 = - Vector4(getValue(0), getValue(1), getValue(2), getValue(3)) + Vector4(getValue(0), getValue(1), getValue(2), getValue(3)) } inner class RGBaChannel(keys: Array, defaultValue: ColorRGBa = ColorRGBa.WHITE) : - CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.alpha)) { + CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.alpha)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa = - ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3)) + ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3)) } inner class RGBChannel(keys: Array, defaultValue: ColorRGBa = ColorRGBa.WHITE) : - CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) { + CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa = - ColorRGBa(getValue(0), getValue(1), getValue(2)) + ColorRGBa(getValue(0), getValue(1), getValue(2)) } inner class DoubleArrayChannel(keys: Array, defaultValue: DoubleArray = DoubleArray(keys.size)) : - CompoundChannel(keys, defaultValue.toTypedArray()) { + CompoundChannel(keys, defaultValue.toTypedArray()) { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray { val result = DoubleArray(keys.size) for (i in keys.indices) { @@ -108,14 +111,13 @@ open class Keyframer { } } - val channels = mutableMapOf() fun loadFromJson( - file: File, - format: KeyframerFormat = KeyframerFormat.SIMPLE, - parameters: Map = emptyMap(), - functions: FunctionExtensions = FunctionExtensions.EMPTY + file: File, + format: KeyframerFormat = KeyframerFormat.SIMPLE, + parameters: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY ) { require(file.exists()) { "failed to load keyframer from json: '${file.absolutePath}' does not exist." @@ -128,10 +130,10 @@ open class Keyframer { } fun loadFromJson( - url: URL, - format: KeyframerFormat = KeyframerFormat.SIMPLE, - parameters: Map = emptyMap(), - functions: FunctionExtensions = FunctionExtensions.EMPTY + url: URL, + format: KeyframerFormat = KeyframerFormat.SIMPLE, + parameters: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY ) { try { loadFromJsonString(url.readText(), format, parameters, functions) @@ -143,10 +145,10 @@ open class Keyframer { } fun loadFromJsonString( - json: String, - format: KeyframerFormat = KeyframerFormat.SIMPLE, - parameters: Map = emptyMap(), - functions: FunctionExtensions = FunctionExtensions.EMPTY + json: String, + format: KeyframerFormat = KeyframerFormat.SIMPLE, + parameters: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY ) { when (format) { KeyframerFormat.SIMPLE -> { @@ -160,6 +162,7 @@ open class Keyframer { error("Error parsing simple Keyframer data: ${e.cause?.message}") } } + KeyframerFormat.FULL -> { try { val type = object : TypeToken>() {}.type @@ -176,9 +179,9 @@ open class Keyframer { private val prototypes = mutableMapOf>() fun loadFromObjects( - dict: Map, - externalParameters: Map = emptyMap(), - functions: FunctionExtensions = FunctionExtensions.EMPTY + dict: Map, + externalParameters: Map = emptyMap(), + functions: FunctionExtensions = FunctionExtensions.EMPTY ) { this.parameters.clear() this.parameters.putAll(externalParameters) @@ -191,7 +194,8 @@ open class Keyframer { when (val candidate = entry.value) { is Double -> candidate is String -> evaluateExpression(candidate, parameters, functions) - ?: error("could not evaluate expression: '$candidate'") + ?: error("could not evaluate expression: '$candidate'") + is Int -> candidate.toDouble() is Float -> candidate.toDouble() else -> error("unknown type for parameter '${entry.key}'") @@ -226,9 +230,9 @@ open class Keyframer { } fun loadFromKeyObjects( - keys: List>, - externalParameters: Map, - functions: FunctionExtensions + keys: List>, + externalParameters: Map, + functions: FunctionExtensions ) { if (externalParameters !== parameters) { parameters.clear() @@ -238,12 +242,12 @@ open class Keyframer { var lastTime = 0.0 val channelDelegates = this::class.memberProperties - .mapNotNull { - @Suppress("UNCHECKED_CAST") - it as? KProperty1 - } - .filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel } - .associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) } + .mapNotNull { + @Suppress("UNCHECKED_CAST") + it as? KProperty1 + } + .filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel } + .associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) } val channelKeys = channelDelegates.values.flatMap { channel -> channel.keys.map { it } @@ -310,7 +314,8 @@ open class Keyframer { when (val candidate = computed["time"]) { null -> lastTime is String -> evaluateExpression(candidate, expressionContext, functions) - ?: error { "unknown value format for time : $candidate" } + ?: error { "unknown value format for time : $candidate" } + is Double -> candidate is Int -> candidate.toDouble() is Float -> candidate.toDouble() @@ -324,7 +329,8 @@ open class Keyframer { when (val candidate = computed["duration"]) { null -> 0.0 is String -> evaluateExpression(candidate, expressionContext, functions) - ?: error { "unknown value format for time : $candidate" } + ?: error { "unknown value format for time : $candidate" } + is Int -> candidate.toDouble() is Float -> candidate.toDouble() is Double -> candidate @@ -380,7 +386,8 @@ open class Keyframer { null -> error("no value for '${channelCandidate.key}'") is Double -> candidate is String -> evaluateExpression(candidate, expressionContext, functions) - ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + is Int -> candidate.toDouble() else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") } @@ -395,11 +402,11 @@ open class Keyframer { } val dictEnvelope = when (val candidate = valueMap["envelope"]) { - null -> envelope - is DoubleArray -> candidate - is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() - is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() - else -> error("unknown envelope for '$candidate") + null -> envelope + is DoubleArray -> candidate + is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() + is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() + else -> error("unknown envelope for '$candidate") } val dictDuration = try { @@ -407,7 +414,8 @@ open class Keyframer { null -> null is Double -> candidate is String -> evaluateExpression(candidate, expressionContext, functions) - ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + is Int -> candidate.toDouble() else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") } @@ -417,7 +425,12 @@ open class Keyframer { if (dictDuration != null) { if (dictDuration <= 0.0) { - channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, defaultEnvelope) + channel.add( + max(lastTime, time + dictDuration), + lastValue, + Easing.Linear.function, + defaultEnvelope + ) channel.add(time, value, dictEasing, dictEnvelope) } else { channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope) @@ -432,7 +445,8 @@ open class Keyframer { when (val candidate = channelCandidate.value) { is Double -> candidate is String -> evaluateExpression(candidate, expressionContext, functions) - ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + ?: error("unknown value format for key '${channelCandidate.key}' : $candidate") + is Int -> candidate.toDouble() else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") } diff --git a/orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt b/orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt index 46ac46f2..d3618d56 100644 --- a/orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt +++ b/orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt @@ -1,7 +1,8 @@ import org.amshove.kluent.`should throw` import org.amshove.kluent.invoking +import org.openrndr.extra.expressions.ExpressionException import kotlin.test.Test -import org.openrndr.extra.keyframer.ExpressionException + import org.openrndr.extra.keyframer.Keyframer import org.openrndr.extra.keyframer.KeyframerFormat import java.io.File diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b7728bc..a56461b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include( "orx-compute-graph-nodes", "orx-jvm:orx-dnk3", "orx-easing", + "orx-jvm:orx-expression-evaluator", "orx-jvm:orx-file-watcher", "orx-parameters", "orx-fx",