[orx-expression-evaluator, orx-keyframer] Split expression evaluator from orx-keyframer

This commit is contained in:
Edwin Jakobs
2023-03-29 13:34:55 +02:00
parent 606e56be3d
commit 8940fb7520
20 changed files with 685 additions and 199 deletions

View File

@@ -0,0 +1,2 @@
*.tokens
gen/

View File

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

View File

@@ -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,32 +19,19 @@ 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<KotlinCompile> {
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"))
}

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
lexer grammar KeyLangLexer;
@header {
package org.openrndr.extra.keyframer.antlr;
}
channels { WHITESPACE }
// Whitespace

View File

@@ -1,10 +1,6 @@
parser grammar KeyLangParser;
@header {
package org.openrndr.extra.keyframer.antlr;
}
options { tokenVocab=KeyLangLexer; }
keyLangFile : lines=line+ ;

View File

@@ -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<String, Double> = mapOf(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (Double) -> Double = { 0.0 },
): (Double) -> Double {
require(!constants.containsKey(parameter0))
try {
val root = expressionRoot(expression)
val variables = mutableMapOf<String, Double>()
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<String, Double> = 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<String, Double>()
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<String, Double> = 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<String, Double>()
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
}
}

View File

@@ -0,0 +1,40 @@
package org.openrndr.extra.expressions
import org.openrndr.extra.propertywatchers.watchingProperty
import kotlin.reflect.KProperty0
fun watchingExpression1(
expressionProperty: KProperty0<String>,
parameter0: String = "x",
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double) -> Double = { 0.0 }
) =
watchingProperty(expressionProperty) {
compileFunction1(it, parameter0, constants, functions, error)
}
fun watchingExpression2(
expressionProperty: KProperty0<String>,
parameter0: String = "x",
parameter1: String = "y",
constants: Map<String, Double> = 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<String>,
parameter0: String = "x",
parameter1: String = "y",
parameter2: String = "z",
constants: Map<String, Double> = 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)
}

View File

@@ -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.*
@@ -41,11 +41,14 @@ internal enum class IDType {
FUNCTION5
}
internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) :
internal class ExpressionListener(
val functions: FunctionExtensions = FunctionExtensions.EMPTY,
val constants: Map<String, Double> = mapOf()
) :
KeyLangParserBaseListener() {
val doubleStack = Stack<Double>()
val functionStack = Stack<(DoubleArray) -> Double>()
val variables = mutableMapOf<String, Double>()
val idTypeStack = Stack<IDType>()
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,13 +251,13 @@ 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)
else -> constants[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)
@@ -295,6 +298,7 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
}
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double =
when (name) {
@@ -312,6 +316,7 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
}
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double =
when (name) {
@@ -321,20 +326,38 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
"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]) } }
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]) } }
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") }
@@ -349,13 +372,24 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
"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]) } }
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,11 +399,11 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
class ExpressionException(message: String) : RuntimeException(message)
fun evaluateExpression(
input: String,
variables: Map<String, Double> = emptyMap(),
expression: String,
constants: Map<String, Double> = 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() {
@@ -381,13 +415,12 @@ fun evaluateExpression(
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) {
@@ -395,3 +428,57 @@ fun evaluateExpression(
}
return listener.lastExpressionResult
}
fun compileExpression(
expression: String,
constants: Map<String, Double> = 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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
org.openrndr.extra.convention.`kotlin-jvm`
}
tasks.withType<KotlinCompile> {
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)
}

View File

@@ -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
@@ -108,7 +111,6 @@ open class Keyframer {
}
}
val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson(
@@ -160,6 +162,7 @@ open class Keyframer {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
}
}
KeyframerFormat.FULL -> {
try {
val type = object : TypeToken<Map<String, Any>>() {}.type
@@ -192,6 +195,7 @@ open class Keyframer {
is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'")
@@ -311,6 +315,7 @@ open class Keyframer {
null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Double -> candidate
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
@@ -325,6 +330,7 @@ open class Keyframer {
null -> 0.0
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
is Double -> candidate
@@ -381,6 +387,7 @@ open class Keyframer {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
@@ -408,6 +415,7 @@ open class Keyframer {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: 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)
@@ -433,6 +446,7 @@ open class Keyframer {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}

View File

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

View File

@@ -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",