[orx-expression-evaluator, orx-keyframer] Split expression evaluator from orx-keyframer
This commit is contained in:
2
orx-jvm/orx-expression-evaluator/.gitignore
vendored
Normal file
2
orx-jvm/orx-expression-evaluator/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.tokens
|
||||||
|
gen/
|
||||||
82
orx-jvm/orx-expression-evaluator/README.md
Normal file
82
orx-jvm/orx-expression-evaluator/README.md
Normal 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)
|
||||||
|
```
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.openrndr.extra.convention.kotlin-jvm")
|
org.openrndr.extra.convention.`kotlin-jvm`
|
||||||
id("antlr")
|
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 {
|
sourceSets {
|
||||||
@@ -11,36 +19,23 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.withType<KotlinCompile> {
|
||||||
useJUnitPlatform {
|
kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||||
includeEngines("spek2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateGrammarSource {
|
|
||||||
maxHeapSize = "64m"
|
|
||||||
arguments += ["-visitor", "-long-messages"]
|
|
||||||
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
antlr(libs.antlr.core)
|
antlr(libs.antlr.core)
|
||||||
implementation(libs.antlr.runtime)
|
implementation(libs.antlr.runtime)
|
||||||
implementation(project(":orx-noise"))
|
|
||||||
implementation(project(":orx-easing"))
|
|
||||||
implementation(libs.openrndr.application)
|
implementation(libs.openrndr.application)
|
||||||
implementation(libs.openrndr.math)
|
implementation(libs.openrndr.math)
|
||||||
implementation(libs.gson)
|
implementation(libs.kotlin.coroutines)
|
||||||
implementation(libs.kotlin.reflect)
|
implementation(project(":orx-property-watchers"))
|
||||||
|
implementation(project(":orx-noise"))
|
||||||
testImplementation(libs.kluent)
|
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"))
|
demoImplementation(project(":orx-jvm:orx-gui"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
|
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
|
||||||
tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource")
|
tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource")
|
||||||
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")
|
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")
|
||||||
tasks.getByName("sourcesJar").dependsOn("generateGrammarSource")
|
tasks.getByName("sourcesJar").dependsOn("generateGrammarSource")
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
|
import org.openrndr.extra.expressions.evaluateExpression
|
||||||
import org.openrndr.extra.gui.GUI
|
import org.openrndr.extra.gui.GUI
|
||||||
import org.openrndr.extra.gui.addTo
|
import org.openrndr.extra.gui.addTo
|
||||||
import org.openrndr.extra.keyframer.evaluateExpression
|
|
||||||
import org.openrndr.extra.parameters.TextParameter
|
import org.openrndr.extra.parameters.TextParameter
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
@@ -13,9 +13,11 @@ fun main() {
|
|||||||
val settings = object {
|
val settings = object {
|
||||||
@TextParameter("x expression", order = 10)
|
@TextParameter("x expression", order = 10)
|
||||||
var xExpression = "cos(t) * 50.0 + width / 2.0"
|
var xExpression = "cos(t) * 50.0 + width / 2.0"
|
||||||
|
|
||||||
@TextParameter("y expression", order = 20)
|
@TextParameter("y expression", order = 20)
|
||||||
var yExpression = "sin(t) * 50.0 + height / 2.0"
|
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"
|
var radiusExpression = "cos(t) * 50.0 + 50.0"
|
||||||
}.addTo(gui)
|
}.addTo(gui)
|
||||||
|
|
||||||
@@ -23,10 +25,15 @@ fun main() {
|
|||||||
extend {
|
extend {
|
||||||
//gui.visible = mouse.position.x < 200.0
|
//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 =
|
fun eval(expression: String): Double =
|
||||||
try { evaluateExpression(expression, expressionContext) ?: 0.0 } catch (e: Throwable) { 0.0 }
|
try {
|
||||||
|
evaluateExpression(expression, expressionContext) ?: 0.0
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
val x = eval(settings.xExpression)
|
val x = eval(settings.xExpression)
|
||||||
val y = eval(settings.yExpression)
|
val y = eval(settings.yExpression)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
lexer grammar KeyLangLexer;
|
lexer grammar KeyLangLexer;
|
||||||
|
|
||||||
@header {
|
|
||||||
package org.openrndr.extra.keyframer.antlr;
|
|
||||||
}
|
|
||||||
|
|
||||||
channels { WHITESPACE }
|
channels { WHITESPACE }
|
||||||
|
|
||||||
// Whitespace
|
// Whitespace
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
|
|
||||||
parser grammar KeyLangParser;
|
parser grammar KeyLangParser;
|
||||||
|
|
||||||
@header {
|
|
||||||
package org.openrndr.extra.keyframer.antlr;
|
|
||||||
}
|
|
||||||
|
|
||||||
options { tokenVocab=KeyLangLexer; }
|
options { tokenVocab=KeyLangLexer; }
|
||||||
|
|
||||||
keyLangFile : lines=line+ ;
|
keyLangFile : lines=line+ ;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.openrndr.extra.keyframer
|
package org.openrndr.extra.expressions
|
||||||
|
|
||||||
import org.antlr.v4.runtime.*
|
import org.antlr.v4.runtime.*
|
||||||
import org.antlr.v4.runtime.tree.ParseTreeWalker
|
import org.antlr.v4.runtime.tree.ParseTreeWalker
|
||||||
import org.antlr.v4.runtime.tree.TerminalNode
|
import org.antlr.v4.runtime.tree.TerminalNode
|
||||||
import org.openrndr.extra.keyframer.antlr.KeyLangLexer
|
import org.openrndr.extra.expressions.antlr.KeyLangLexer
|
||||||
import org.openrndr.extra.keyframer.antlr.KeyLangParser
|
import org.openrndr.extra.expressions.antlr.KeyLangParser
|
||||||
import org.openrndr.extra.keyframer.antlr.KeyLangParserBaseListener
|
import org.openrndr.extra.expressions.antlr.KeyLangParserBaseListener
|
||||||
import org.openrndr.extra.noise.uniform
|
import org.openrndr.extra.noise.uniform
|
||||||
import org.openrndr.math.*
|
import org.openrndr.math.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -19,12 +19,12 @@ typealias Function4 = (Double, Double, Double, Double) -> Double
|
|||||||
typealias Function5 = (Double, Double, Double, Double, Double) -> Double
|
typealias Function5 = (Double, Double, Double, Double, Double) -> Double
|
||||||
|
|
||||||
class FunctionExtensions(
|
class FunctionExtensions(
|
||||||
val functions0: Map<String, Function0> = emptyMap(),
|
val functions0: Map<String, Function0> = emptyMap(),
|
||||||
val functions1: Map<String, Function1> = emptyMap(),
|
val functions1: Map<String, Function1> = emptyMap(),
|
||||||
val functions2: Map<String, Function2> = emptyMap(),
|
val functions2: Map<String, Function2> = emptyMap(),
|
||||||
val functions3: Map<String, Function3> = emptyMap(),
|
val functions3: Map<String, Function3> = emptyMap(),
|
||||||
val functions4: Map<String, Function4> = emptyMap(),
|
val functions4: Map<String, Function4> = emptyMap(),
|
||||||
val functions5: Map<String, Function5> = emptyMap()
|
val functions5: Map<String, Function5> = emptyMap()
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = FunctionExtensions()
|
val EMPTY = FunctionExtensions()
|
||||||
@@ -41,11 +41,14 @@ internal enum class IDType {
|
|||||||
FUNCTION5
|
FUNCTION5
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) :
|
internal class ExpressionListener(
|
||||||
KeyLangParserBaseListener() {
|
val functions: FunctionExtensions = FunctionExtensions.EMPTY,
|
||||||
|
val constants: Map<String, Double> = mapOf()
|
||||||
|
) :
|
||||||
|
KeyLangParserBaseListener() {
|
||||||
val doubleStack = Stack<Double>()
|
val doubleStack = Stack<Double>()
|
||||||
val functionStack = Stack<(DoubleArray) -> Double>()
|
val functionStack = Stack<(DoubleArray) -> Double>()
|
||||||
val variables = mutableMapOf<String, Double>()
|
|
||||||
|
|
||||||
val idTypeStack = Stack<IDType>()
|
val idTypeStack = Stack<IDType>()
|
||||||
var lastExpressionResult: Double? = null
|
var lastExpressionResult: Double? = null
|
||||||
@@ -61,10 +64,10 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
|
|||||||
lastExpressionResult = result
|
lastExpressionResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) {
|
// override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) {
|
||||||
val value = doubleStack.pop()
|
// val value = doubleStack.pop()
|
||||||
variables[ctx.ID()?.text ?: error("buh")] = value
|
// variables[ctx.ID()?.text ?: error("buh")] = value
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
|
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
|
||||||
val op = doubleStack.pop()
|
val op = doubleStack.pop()
|
||||||
@@ -248,114 +251,145 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
|
|||||||
doubleStack.push(node.text.toDouble())
|
doubleStack.push(node.text.toDouble())
|
||||||
}
|
}
|
||||||
if (type == KeyLangParser.ID) {
|
if (type == KeyLangParser.ID) {
|
||||||
val name = node.text.replace("`","")
|
val name = node.text.replace("`", "")
|
||||||
@Suppress("DIVISION_BY_ZERO")
|
@Suppress("DIVISION_BY_ZERO")
|
||||||
when (val idType = idTypeStack.pop()) {
|
when (val idType = idTypeStack.pop()) {
|
||||||
IDType.VARIABLE -> doubleStack.push(
|
IDType.VARIABLE -> doubleStack.push(
|
||||||
when (name) {
|
when (name) {
|
||||||
"PI" -> PI
|
"PI" -> PI
|
||||||
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
|
else -> constants[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
IDType.FUNCTION0 -> {
|
IDType.FUNCTION0 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"random" -> { _ -> Double.uniform(0.0, 1.0) }
|
"random" -> { _ -> Double.uniform(0.0, 1.0) }
|
||||||
else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } }
|
else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } }
|
||||||
?: errorValue(
|
?: errorValue(
|
||||||
"unresolved function: '${name}()'"
|
"unresolved function: '${name}()'"
|
||||||
) { _ -> error("this is the error function") }
|
) { _ -> error("this is the error function") }
|
||||||
}
|
}
|
||||||
functionStack.push(function)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
IDType.FUNCTION1 -> {
|
IDType.FUNCTION1 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"sqrt" -> { x -> sqrt(x[0]) }
|
"sqrt" -> { x -> sqrt(x[0]) }
|
||||||
"radians" -> { x -> Math.toRadians(x[0]) }
|
"radians" -> { x -> Math.toRadians(x[0]) }
|
||||||
"degrees" -> { x -> Math.toDegrees(x[0]) }
|
"degrees" -> { x -> Math.toDegrees(x[0]) }
|
||||||
"cos" -> { x -> cos(x[0]) }
|
"cos" -> { x -> cos(x[0]) }
|
||||||
"sin" -> { x -> sin(x[0]) }
|
"sin" -> { x -> sin(x[0]) }
|
||||||
"tan" -> { x -> tan(x[0]) }
|
"tan" -> { x -> tan(x[0]) }
|
||||||
"atan" -> { x -> atan(x[0]) }
|
"atan" -> { x -> atan(x[0]) }
|
||||||
"acos" -> { x -> acos(x[0]) }
|
"acos" -> { x -> acos(x[0]) }
|
||||||
"asin" -> { x -> asin(x[0]) }
|
"asin" -> { x -> asin(x[0]) }
|
||||||
"exp" -> { x -> exp(x[0]) }
|
"exp" -> { x -> exp(x[0]) }
|
||||||
"abs" -> { x -> abs(x[0]) }
|
"abs" -> { x -> abs(x[0]) }
|
||||||
"floor" -> { x -> floor(x[0]) }
|
"floor" -> { x -> floor(x[0]) }
|
||||||
"round" -> { x -> round(x[0]) }
|
"round" -> { x -> round(x[0]) }
|
||||||
"ceil" -> { x -> ceil(x[0]) }
|
"ceil" -> { x -> ceil(x[0]) }
|
||||||
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
|
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
|
||||||
else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
|
else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
|
||||||
?: errorValue(
|
?: errorValue(
|
||||||
"unresolved function: '${name}(x0)'"
|
"unresolved function: '${name}(x0)'"
|
||||||
) { _ -> error("this is the error function") }
|
) { _ -> error("this is the error function") }
|
||||||
}
|
}
|
||||||
functionStack.push(function)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
IDType.FUNCTION2 -> {
|
IDType.FUNCTION2 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"max" -> { x -> max(x[0], x[1]) }
|
"max" -> { x -> max(x[0], x[1]) }
|
||||||
"min" -> { x -> min(x[0], x[1]) }
|
"min" -> { x -> min(x[0], x[1]) }
|
||||||
"pow" -> { x -> x[0].pow(x[1]) }
|
"pow" -> { x -> x[0].pow(x[1]) }
|
||||||
"mod" -> { x -> x[0].mod(x[1]) }
|
"mod" -> { x -> x[0].mod(x[1]) }
|
||||||
"atan2" -> { x -> atan2(x[0], x[1]) }
|
"atan2" -> { x -> atan2(x[0], x[1]) }
|
||||||
"random" -> { x -> Double.uniform(x[0], x[1]) }
|
"random" -> { x -> Double.uniform(x[0], x[1]) }
|
||||||
"length" -> { x -> Vector2(x[0], x[1]).length }
|
"length" -> { x -> Vector2(x[0], x[1]).length }
|
||||||
else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
|
else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
|
||||||
?: errorValue(
|
?: errorValue(
|
||||||
"unresolved function: '${name}(x0, x1)'"
|
"unresolved function: '${name}(x0, x1)'"
|
||||||
) { _ -> error("this is the error function") }
|
) { _ -> error("this is the error function") }
|
||||||
}
|
}
|
||||||
functionStack.push(function)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
IDType.FUNCTION3 -> {
|
IDType.FUNCTION3 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"mix" -> { x -> mix(x[0], x[1], x[2]) }
|
"mix" -> { x -> mix(x[0], x[1], x[2]) }
|
||||||
"min" -> { x -> x.minOrNull()!! }
|
"min" -> { x -> x.minOrNull()!! }
|
||||||
"max" -> { x -> x.maxOrNull()!! }
|
"max" -> { x -> x.maxOrNull()!! }
|
||||||
"sum" -> { x -> x.sum() }
|
"sum" -> { x -> x.sum() }
|
||||||
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
|
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
|
||||||
"length" -> { x -> Vector3(x[0], x[1], x[2]).length }
|
"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 {
|
||||||
?: errorValue(
|
{ x: DoubleArray ->
|
||||||
"unresolved function: '${name}(x0, x1, x2)'"
|
it.invoke(
|
||||||
) { _ -> error("this is the error function") }
|
x[0],
|
||||||
|
x[1],
|
||||||
|
x[2]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
?: errorValue(
|
||||||
|
"unresolved function: '${name}(x0, x1, x2)'"
|
||||||
|
) { _ -> error("this is the error function") }
|
||||||
|
}
|
||||||
functionStack.push(function)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
IDType.FUNCTION4 -> {
|
IDType.FUNCTION4 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"min" -> { x -> x.minOrNull()!! }
|
"min" -> { x -> x.minOrNull()!! }
|
||||||
"max" -> { x -> x.maxOrNull()!! }
|
"max" -> { x -> x.maxOrNull()!! }
|
||||||
"sum" -> { x -> x.sum() }
|
"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 {
|
||||||
?: errorValue(
|
{ x: DoubleArray ->
|
||||||
"unresolved function: '${name}(x0, x1, x2, x3)'"
|
it.invoke(
|
||||||
) { _ -> error("this is the error function") }
|
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)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
IDType.FUNCTION5 -> {
|
IDType.FUNCTION5 -> {
|
||||||
val function: (DoubleArray) -> Double =
|
val function: (DoubleArray) -> Double =
|
||||||
when (name) {
|
when (name) {
|
||||||
"min" -> { x -> x.minOrNull()!! }
|
"min" -> { x -> x.minOrNull()!! }
|
||||||
"max" -> { x -> x.maxOrNull()!! }
|
"max" -> { x -> x.maxOrNull()!! }
|
||||||
"sum" -> { x -> x.sum() }
|
"sum" -> { x -> x.sum() }
|
||||||
"map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) }
|
"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 {
|
||||||
?: errorValue(
|
{ x: DoubleArray ->
|
||||||
"unresolved function: '${name}(x0, x1, x2, x3, x4)'"
|
it.invoke(
|
||||||
) { _ -> error("this is the error function") }
|
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)
|
functionStack.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("unsupported id-type $idType")
|
else -> error("unsupported id-type $idType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,33 +399,86 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
|
|||||||
class ExpressionException(message: String) : RuntimeException(message)
|
class ExpressionException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
fun evaluateExpression(
|
fun evaluateExpression(
|
||||||
input: String,
|
expression: String,
|
||||||
variables: Map<String, Double> = emptyMap(),
|
constants: Map<String, Double> = emptyMap(),
|
||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
): Double? {
|
): Double? {
|
||||||
val lexer = KeyLangLexer(CharStreams.fromString(input))
|
val lexer = KeyLangLexer(CharStreams.fromString(expression))
|
||||||
val parser = KeyLangParser(CommonTokenStream(lexer))
|
val parser = KeyLangParser(CommonTokenStream(lexer))
|
||||||
parser.removeErrorListeners()
|
parser.removeErrorListeners()
|
||||||
parser.addErrorListener(object : BaseErrorListener() {
|
parser.addErrorListener(object : BaseErrorListener() {
|
||||||
override fun syntaxError(
|
override fun syntaxError(
|
||||||
recognizer: Recognizer<*, *>?,
|
recognizer: Recognizer<*, *>?,
|
||||||
offendingSymbol: Any?,
|
offendingSymbol: Any?,
|
||||||
line: Int,
|
line: Int,
|
||||||
charPositionInLine: Int,
|
charPositionInLine: Int,
|
||||||
msg: String?,
|
msg: String?,
|
||||||
e: RecognitionException?
|
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 root = parser.keyLangFile()
|
||||||
val listener = ExpressionListener(functions)
|
val listener = ExpressionListener(functions, constants)
|
||||||
listener.variables.putAll(variables)
|
|
||||||
try {
|
try {
|
||||||
ParseTreeWalker.DEFAULT.walk(listener, root)
|
ParseTreeWalker.DEFAULT.walk(listener, root)
|
||||||
} catch (e: ExpressionException) {
|
} catch (e: ExpressionException) {
|
||||||
throw ExpressionException(e.message ?: "")
|
throw ExpressionException(e.message ?: "")
|
||||||
}
|
}
|
||||||
return listener.lastExpressionResult
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import org.amshove.kluent.`should throw`
|
import org.amshove.kluent.`should throw`
|
||||||
import org.amshove.kluent.`with message`
|
import org.amshove.kluent.`with message`
|
||||||
import org.amshove.kluent.invoking
|
import org.amshove.kluent.invoking
|
||||||
import org.openrndr.extra.keyframer.ExpressionException
|
import org.openrndr.extra.expressions.ExpressionException
|
||||||
import org.openrndr.extra.keyframer.evaluateExpression
|
import org.openrndr.extra.expressions.evaluateExpression
|
||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
class TestExpressionErrors {
|
class TestExpressionErrors {
|
||||||
@@ -1,25 +1,21 @@
|
|||||||
import org.amshove.kluent.shouldBe
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.amshove.kluent.shouldBeNear
|
import org.amshove.kluent.shouldBeNear
|
||||||
import org.openrndr.extra.keyframer.FunctionExtensions
|
import org.openrndr.extra.expressions.FunctionExtensions
|
||||||
import org.openrndr.extra.keyframer.evaluateExpression
|
import org.openrndr.extra.expressions.evaluateExpression
|
||||||
import org.openrndr.math.map
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
class TestExpressions {
|
class TestExpressions {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `a value reference`() {
|
fun `a value reference`() {
|
||||||
val expression = "someValue"
|
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)
|
result?.shouldBeEqualTo(5.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `a backticked value reference`() {
|
fun `a backticked value reference`() {
|
||||||
val expression = "`some-value`"
|
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)
|
result?.shouldBeEqualTo(5.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import org.amshove.kluent.shouldBeNear
|
import org.amshove.kluent.shouldBeNear
|
||||||
import org.openrndr.extra.keyframer.evaluateExpression
|
import org.openrndr.extra.expressions.evaluateExpression
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
class TestOperators {
|
class TestOperators {
|
||||||
21
orx-jvm/orx-keyframer/build.gradle.kts
Normal file
21
orx-jvm/orx-keyframer/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ import com.google.gson.reflect.TypeToken
|
|||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.extra.easing.Easing
|
import org.openrndr.extra.easing.Easing
|
||||||
import org.openrndr.extra.easing.EasingFunction
|
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.Vector2
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.math.Vector4
|
import org.openrndr.math.Vector4
|
||||||
@@ -64,41 +67,41 @@ open class Keyframer {
|
|||||||
|
|
||||||
|
|
||||||
inner class DoubleChannel(key: String, defaultValue: Double = 0.0) :
|
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)
|
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) :
|
inner class Vector2Channel(keys: Array<String>, 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))
|
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) :
|
inner class Vector3Channel(keys: Array<String>, 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 =
|
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<String>, defaultValue: Vector4 = Vector4.ZERO) :
|
inner class Vector4Channel(keys: Array<String>, 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 =
|
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<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
|
inner class RGBaChannel(keys: Array<String>, 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 =
|
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<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
|
inner class RGBChannel(keys: Array<String>, 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 =
|
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<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
|
inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
|
||||||
CompoundChannel(keys, defaultValue.toTypedArray()) {
|
CompoundChannel(keys, defaultValue.toTypedArray()) {
|
||||||
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
|
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
|
||||||
val result = DoubleArray(keys.size)
|
val result = DoubleArray(keys.size)
|
||||||
for (i in keys.indices) {
|
for (i in keys.indices) {
|
||||||
@@ -108,14 +111,13 @@ open class Keyframer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val channels = mutableMapOf<String, KeyframerChannel>()
|
val channels = mutableMapOf<String, KeyframerChannel>()
|
||||||
|
|
||||||
fun loadFromJson(
|
fun loadFromJson(
|
||||||
file: File,
|
file: File,
|
||||||
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
||||||
parameters: Map<String, Double> = emptyMap(),
|
parameters: Map<String, Double> = emptyMap(),
|
||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
) {
|
) {
|
||||||
require(file.exists()) {
|
require(file.exists()) {
|
||||||
"failed to load keyframer from json: '${file.absolutePath}' does not exist."
|
"failed to load keyframer from json: '${file.absolutePath}' does not exist."
|
||||||
@@ -128,10 +130,10 @@ open class Keyframer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromJson(
|
fun loadFromJson(
|
||||||
url: URL,
|
url: URL,
|
||||||
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
||||||
parameters: Map<String, Double> = emptyMap(),
|
parameters: Map<String, Double> = emptyMap(),
|
||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
loadFromJsonString(url.readText(), format, parameters, functions)
|
loadFromJsonString(url.readText(), format, parameters, functions)
|
||||||
@@ -143,10 +145,10 @@ open class Keyframer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromJsonString(
|
fun loadFromJsonString(
|
||||||
json: String,
|
json: String,
|
||||||
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
format: KeyframerFormat = KeyframerFormat.SIMPLE,
|
||||||
parameters: Map<String, Double> = emptyMap(),
|
parameters: Map<String, Double> = emptyMap(),
|
||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
) {
|
) {
|
||||||
when (format) {
|
when (format) {
|
||||||
KeyframerFormat.SIMPLE -> {
|
KeyframerFormat.SIMPLE -> {
|
||||||
@@ -160,6 +162,7 @@ open class Keyframer {
|
|||||||
error("Error parsing simple Keyframer data: ${e.cause?.message}")
|
error("Error parsing simple Keyframer data: ${e.cause?.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyframerFormat.FULL -> {
|
KeyframerFormat.FULL -> {
|
||||||
try {
|
try {
|
||||||
val type = object : TypeToken<Map<String, Any>>() {}.type
|
val type = object : TypeToken<Map<String, Any>>() {}.type
|
||||||
@@ -176,9 +179,9 @@ open class Keyframer {
|
|||||||
private val prototypes = mutableMapOf<String, Map<String, Any>>()
|
private val prototypes = mutableMapOf<String, Map<String, Any>>()
|
||||||
|
|
||||||
fun loadFromObjects(
|
fun loadFromObjects(
|
||||||
dict: Map<String, Any>,
|
dict: Map<String, Any>,
|
||||||
externalParameters: Map<String, Double> = emptyMap(),
|
externalParameters: Map<String, Double> = emptyMap(),
|
||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
) {
|
) {
|
||||||
this.parameters.clear()
|
this.parameters.clear()
|
||||||
this.parameters.putAll(externalParameters)
|
this.parameters.putAll(externalParameters)
|
||||||
@@ -191,7 +194,8 @@ open class Keyframer {
|
|||||||
when (val candidate = entry.value) {
|
when (val candidate = entry.value) {
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
is String -> evaluateExpression(candidate, parameters, functions)
|
is String -> evaluateExpression(candidate, parameters, functions)
|
||||||
?: error("could not evaluate expression: '$candidate'")
|
?: error("could not evaluate expression: '$candidate'")
|
||||||
|
|
||||||
is Int -> candidate.toDouble()
|
is Int -> candidate.toDouble()
|
||||||
is Float -> candidate.toDouble()
|
is Float -> candidate.toDouble()
|
||||||
else -> error("unknown type for parameter '${entry.key}'")
|
else -> error("unknown type for parameter '${entry.key}'")
|
||||||
@@ -226,9 +230,9 @@ open class Keyframer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromKeyObjects(
|
fun loadFromKeyObjects(
|
||||||
keys: List<Map<String, Any>>,
|
keys: List<Map<String, Any>>,
|
||||||
externalParameters: Map<String, Double>,
|
externalParameters: Map<String, Double>,
|
||||||
functions: FunctionExtensions
|
functions: FunctionExtensions
|
||||||
) {
|
) {
|
||||||
if (externalParameters !== parameters) {
|
if (externalParameters !== parameters) {
|
||||||
parameters.clear()
|
parameters.clear()
|
||||||
@@ -238,12 +242,12 @@ open class Keyframer {
|
|||||||
var lastTime = 0.0
|
var lastTime = 0.0
|
||||||
|
|
||||||
val channelDelegates = this::class.memberProperties
|
val channelDelegates = this::class.memberProperties
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
it as? KProperty1<Keyframer, Any>
|
it as? KProperty1<Keyframer, Any>
|
||||||
}
|
}
|
||||||
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
|
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
|
||||||
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
|
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
|
||||||
|
|
||||||
val channelKeys = channelDelegates.values.flatMap { channel ->
|
val channelKeys = channelDelegates.values.flatMap { channel ->
|
||||||
channel.keys.map { it }
|
channel.keys.map { it }
|
||||||
@@ -310,7 +314,8 @@ open class Keyframer {
|
|||||||
when (val candidate = computed["time"]) {
|
when (val candidate = computed["time"]) {
|
||||||
null -> lastTime
|
null -> lastTime
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)
|
is String -> evaluateExpression(candidate, expressionContext, functions)
|
||||||
?: error { "unknown value format for time : $candidate" }
|
?: error { "unknown value format for time : $candidate" }
|
||||||
|
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
is Int -> candidate.toDouble()
|
is Int -> candidate.toDouble()
|
||||||
is Float -> candidate.toDouble()
|
is Float -> candidate.toDouble()
|
||||||
@@ -324,7 +329,8 @@ open class Keyframer {
|
|||||||
when (val candidate = computed["duration"]) {
|
when (val candidate = computed["duration"]) {
|
||||||
null -> 0.0
|
null -> 0.0
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)
|
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 Int -> candidate.toDouble()
|
||||||
is Float -> candidate.toDouble()
|
is Float -> candidate.toDouble()
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
@@ -380,7 +386,8 @@ open class Keyframer {
|
|||||||
null -> error("no value for '${channelCandidate.key}'")
|
null -> error("no value for '${channelCandidate.key}'")
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)
|
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()
|
is Int -> candidate.toDouble()
|
||||||
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
||||||
}
|
}
|
||||||
@@ -395,11 +402,11 @@ open class Keyframer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val dictEnvelope = when (val candidate = valueMap["envelope"]) {
|
val dictEnvelope = when (val candidate = valueMap["envelope"]) {
|
||||||
null -> envelope
|
null -> envelope
|
||||||
is DoubleArray -> candidate
|
is DoubleArray -> candidate
|
||||||
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
|
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
|
||||||
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
|
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
|
||||||
else -> error("unknown envelope for '$candidate")
|
else -> error("unknown envelope for '$candidate")
|
||||||
|
|
||||||
}
|
}
|
||||||
val dictDuration = try {
|
val dictDuration = try {
|
||||||
@@ -407,7 +414,8 @@ open class Keyframer {
|
|||||||
null -> null
|
null -> null
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)
|
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()
|
is Int -> candidate.toDouble()
|
||||||
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
||||||
}
|
}
|
||||||
@@ -417,7 +425,12 @@ open class Keyframer {
|
|||||||
|
|
||||||
if (dictDuration != null) {
|
if (dictDuration != null) {
|
||||||
if (dictDuration <= 0.0) {
|
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)
|
channel.add(time, value, dictEasing, dictEnvelope)
|
||||||
} else {
|
} else {
|
||||||
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
|
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
|
||||||
@@ -432,7 +445,8 @@ open class Keyframer {
|
|||||||
when (val candidate = channelCandidate.value) {
|
when (val candidate = channelCandidate.value) {
|
||||||
is Double -> candidate
|
is Double -> candidate
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)
|
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()
|
is Int -> candidate.toDouble()
|
||||||
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import org.amshove.kluent.`should throw`
|
import org.amshove.kluent.`should throw`
|
||||||
import org.amshove.kluent.invoking
|
import org.amshove.kluent.invoking
|
||||||
|
import org.openrndr.extra.expressions.ExpressionException
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import org.openrndr.extra.keyframer.ExpressionException
|
|
||||||
import org.openrndr.extra.keyframer.Keyframer
|
import org.openrndr.extra.keyframer.Keyframer
|
||||||
import org.openrndr.extra.keyframer.KeyframerFormat
|
import org.openrndr.extra.keyframer.KeyframerFormat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ include(
|
|||||||
"orx-compute-graph-nodes",
|
"orx-compute-graph-nodes",
|
||||||
"orx-jvm:orx-dnk3",
|
"orx-jvm:orx-dnk3",
|
||||||
"orx-easing",
|
"orx-easing",
|
||||||
|
"orx-jvm:orx-expression-evaluator",
|
||||||
"orx-jvm:orx-file-watcher",
|
"orx-jvm:orx-file-watcher",
|
||||||
"orx-parameters",
|
"orx-parameters",
|
||||||
"orx-fx",
|
"orx-fx",
|
||||||
|
|||||||
Reference in New Issue
Block a user