commit c4b134bb801140050f9e2108e77d1ee082f8bff8 Author: Moritz Ruth Date: Sun May 21 18:57:16 2023 +0200 commit #1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72b8d7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +/.gradle/ +/run/ + +/.idea/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ed115b5 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + application +} + +group = "de.moritzruth.lampenfieber" +version = "1.0-SNAPSHOT" + +allprojects { + tasks.withType { + kotlinOptions.jvmTarget = "16" + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.ExperimentalUnsignedTypes" + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.contracts.ExperimentalContracts" + kotlinOptions.freeCompilerArgs += "-Xcontext-receivers" + } +} + +dependencies { + implementation(KotlinX.coroutines.core) + implementation(KotlinX.collections.immutable) + implementation(KotlinX.serialization.json) + implementation(KotlinX.datetime) + implementation("org.slf4j:slf4j-simple:_") + implementation("com.fazecast:jSerialComm:_") + implementation("io.github.oshai:kotlin-logging-jvm:_") +} + +application { + mainClass.set("de.moritzruth.lampenfieber.MainKt") +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2841f34 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +org.gradle.jvmargs=-Xmx4G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..60c76b3 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c64934d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} + +plugins { + id("de.fayard.refreshVersions") version "0.51.0" +} + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositories { + mavenCentral() + } +} + +rootProject.name = "Lampenfieber" diff --git a/src/main/kotlin/de/moritzruth/lampenfieber/FloorBarDevice.kt b/src/main/kotlin/de/moritzruth/lampenfieber/FloorBarDevice.kt new file mode 100644 index 0000000..246fb56 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/lampenfieber/FloorBarDevice.kt @@ -0,0 +1,22 @@ +package de.moritzruth.lampenfieber + +import de.moritzruth.theaterdsl.* +import de.moritzruth.theaterdsl.dmx.DmxAddress +import de.moritzruth.theaterdsl.dmx.DmxDataWriter + +class FloorBarDevice(override val firstChannel: DmxAddress): Device { + override val numberOfChannels: UInt = 5u + + override fun writeDmxData(writer: DmxDataWriter) { + val (red, green, blue) = color.getCurrentValue().getRGB() + writer.writePercentage(red) + writer.writePercentage(green) + writer.writePercentage(blue) + writer.writePercentage(brightness.getCurrentValue()) + writer.writePercentage(strobeSpeed.getCurrentValue()) + } + + val color = ColorDV() + val brightness = PercentageDV() + val strobeSpeed = PercentageDV() +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/lampenfieber/Main.kt b/src/main/kotlin/de/moritzruth/lampenfieber/Main.kt new file mode 100644 index 0000000..d223d47 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/lampenfieber/Main.kt @@ -0,0 +1,50 @@ +package de.moritzruth.lampenfieber + +import de.moritzruth.theaterdsl.StepTrigger +import de.moritzruth.theaterdsl.createAct +import de.moritzruth.theaterdsl.dmx.DmxAddress +import de.moritzruth.theaterdsl.dmx.EnttecOpenDmxUsb +import de.moritzruth.theaterdsl.runShow +import de.moritzruth.theaterdsl.value.Color +import de.moritzruth.theaterdsl.value.degrees +import de.moritzruth.theaterdsl.value.percent +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +val bar = FloorBarDevice(DmxAddress(15u)) + +val firstAct = createAct("Erster Akt") { + scene("Prolog") { + step { + trigger = StepTrigger.MusicStart("1. Ouvertüre: Dissonanzen in Schwarz und Weiß", 2.minutes) + + actors { + + "Sophie / von links" + } + + bar.color.static(Color(100.degrees, 100.percent, 100.percent)) + bar.brightness.switch(100.percent, 0.percent, 10.seconds) +// bar.color.fade(Color(100.degrees, 100.percent, 100.percent), 2.seconds) + } + + step { + trigger = StepTrigger.MusicEnd + + actors { + - "Sophie" + } + } + } +} + +suspend fun main() { + EnttecOpenDmxUsb.start() + runShow( + devices = setOf( + bar + ), + acts = listOf( + firstAct + ) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Act.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Act.kt new file mode 100644 index 0000000..378564d --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Act.kt @@ -0,0 +1,5 @@ +package de.moritzruth.theaterdsl + +import kotlinx.collections.immutable.ImmutableList + +class Act(val name: String, val scenes: ImmutableList) \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Device.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Device.kt new file mode 100644 index 0000000..38e5d89 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Device.kt @@ -0,0 +1,10 @@ +package de.moritzruth.theaterdsl + +import de.moritzruth.theaterdsl.dmx.DmxAddress +import de.moritzruth.theaterdsl.dmx.DmxDataWriter + +interface Device { + val firstChannel: DmxAddress + val numberOfChannels: UInt + fun writeDmxData(writer: DmxDataWriter) +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Dsl.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Dsl.kt new file mode 100644 index 0000000..85ef9dc --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Dsl.kt @@ -0,0 +1,142 @@ +package de.moritzruth.theaterdsl + +import io.github.oshai.KotlinLogging +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet + +@DslMarker +annotation class TheaterDslMarker + +@TheaterDslMarker +interface ActBuilderContext { + fun scene(name: String, build: SceneBuilderContext.() -> Unit) +} + +@TheaterDslMarker +interface SceneBuilderContext { + fun step(build: StepDataBuilderContext.() -> Unit) +} + +@TheaterDslMarker +interface StepDataBuilderContext { + var trigger: StepTrigger + val props: PropsBuilderMap + + fun actors(build: ActorsBuildContext.() -> Unit) + fun onRun(block: StepRunner) +} + +class PropsBuilderMap(private val backingMap: MutableMap) { + operator fun set(position: PropPosition, prop: StringWithDetails) = backingMap.set(position, prop) + operator fun set(position: PropPosition, prop: String) = backingMap.set(position, StringWithDetails(prop)) +} + +@TheaterDslMarker +class ActorsBuildContext( + private val entrances: MutableSet, + private val exits: MutableSet +) { + operator fun StringWithDetails.unaryPlus() { + entrances.add(this) + } + + operator fun String.unaryPlus() { + entrances.add(StringWithDetails(this)) + } + + operator fun StringWithDetails.unaryMinus() { + exits.add(this) + } + + operator fun String.unaryMinus() { + exits.add(StringWithDetails(this)) + } +} + +@TheaterDslMarker +fun createAct(name: String, build: ActBuilderContext.() -> Unit): Act { + val scenes = mutableListOf() + val actorsOnStage = mutableListOf() + + object : ActBuilderContext { + override fun scene(name: String, build: SceneBuilderContext.() -> Unit) { + val steps = mutableListOf() + + object : SceneBuilderContext { + override fun step(build: StepDataBuilderContext.() -> Unit) { + var nullableTrigger: StepTrigger? = null + val propsMap = mutableMapOf() + val actorEntrances = mutableSetOf() + val actorExits = mutableSetOf() + var runner: StepRunner? = null + + object : StepDataBuilderContext { + override var trigger: StepTrigger + get() = nullableTrigger ?: throw IllegalStateException("trigger was not set yet") + set(value) { + nullableTrigger = value + } + + override val props = PropsBuilderMap(propsMap) + + override fun actors(build: ActorsBuildContext.() -> Unit) { + ActorsBuildContext(actorEntrances, actorExits).build() + } + + override fun onRun(block: StepRunner) { + runner = block + } + }.build() + + @Suppress("KotlinConstantConditions") + val trigger = nullableTrigger ?: throw IllegalStateException("No trigger was specified") + val logger = KotlinLogging.logger("createAct / $name / #${steps.size + 1} ${trigger.format()}") + + val actorEntrancesNames = actorEntrances.map { it.main } + val actorExitsNames = actorExits.map { it.main } + + runOptionalSanityChecks { + val actorEntrancesDuplicateNames = actorEntrancesNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys + if (actorEntrancesDuplicateNames.isNotEmpty()) + logger.warn("Duplicate actor entrances: ${actorEntrancesDuplicateNames.joinToString()}") + + val actorExitsDuplicateNames = actorExitsNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys + if (actorExitsDuplicateNames.isNotEmpty()) + logger.warn("Duplicate actor exits: ${actorExitsDuplicateNames.joinToString()}") + + val removedAndAddedActorNames = actorEntrancesNames.intersect(actorExitsNames.toSet()) + if (removedAndAddedActorNames.isNotEmpty()) logger.warn( + "These actors are removed and added in the same step: " + + removedAndAddedActorNames.joinToString() + ) + + val removedActorsNotOnStage = actorExitsNames.subtract(actorsOnStage.toSet()) + if (removedActorsNotOnStage.isNotEmpty()) + logger.warn("These actors cannot exit because they are not on the stage: ${removedActorsNotOnStage.joinToString()}") + + val addedActorsAlreadyOnStage = actorsOnStage.intersect(actorEntrancesNames.toSet()) + if (addedActorsAlreadyOnStage.isNotEmpty()) + logger.warn("These actors cannot enter because they are already on the stage: ${removedActorsNotOnStage.joinToString()}") + } + + actorsOnStage.removeAll(actorExitsNames) + actorsOnStage.addAll(actorEntrancesNames) + + steps.add( + Step( + trigger, + actorEntrances.toImmutableSet(), + actorExits.toImmutableSet(), + actorsOnStage.toImmutableList(), + runner + ) + ) + } + }.build() + + scenes.add(Scene(name, steps.toImmutableList())) + } + }.build() + + return Act(name, scenes.toImmutableList()) +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/DynamicValue.kt b/src/main/kotlin/de/moritzruth/theaterdsl/DynamicValue.kt new file mode 100644 index 0000000..8d22c30 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/DynamicValue.kt @@ -0,0 +1,141 @@ +package de.moritzruth.theaterdsl + +import de.moritzruth.theaterdsl.value.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlin.math.PI +import kotlin.math.asin +import kotlin.math.min +import kotlin.math.sin +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource + +interface DynamicValue { + fun getCurrentValue(): T +} + +@OptIn(ExperimentalTime::class) +class PercentageDV(initialStaticValue: Percentage = 0.percent): DynamicValue { + private sealed interface State { + data class Static(val value: Percentage): State + + data class Fade(val start: Percentage, val end: Percentage, val duration: Duration): State { + val delta = end.value - start.value + } + + data class Sine( + val offset: Double, + val minimum: Percentage, + val maximum: Percentage, + val period: Duration + ): State { + companion object { + fun calculateA(minimum: Percentage, maximum: Percentage) = (maximum.value - minimum.value) * 0.5 + fun calculateX(progress: Double, offset: Double) = 2 * PI * progress - offset + fun calculateD(minimum: Percentage) = 0.5 + minimum.value + } + } + + data class Step(val steps: ImmutableList, val interval: Duration, val startIndex: Int): State + } + + private var stateChangeMark = TimeSource.Monotonic.markNow() + private var state: State = State.Static(initialStaticValue) + set(value) { + field = value + stateChangeMark = TimeSource.Monotonic.markNow() + } + + override fun getCurrentValue(): Percentage { + val elapsedTime = stateChangeMark.elapsedNow() + + return when (val s = state) { + is State.Static -> s.value + is State.Fade -> (min(elapsedTime / s.duration, 1.0) * s.delta + s.start.value).toFloat().asPercentage() + is State.Sine -> { + val a = State.Sine.calculateA(s.minimum, s.maximum) + val x = State.Sine.calculateX(elapsedTime / s.period, s.offset) + val d = State.Sine.calculateD(s.minimum) + + (a * sin(x) + d).toFloat().asPercentage() + } + is State.Step -> { + val index = (elapsedTime / s.interval).toInt() + s.startIndex + s.steps[index.mod(s.steps.size)] + } + } + } + + fun static(value: Percentage) { + state = State.Static(value) + } + + fun fade(end: Percentage, duration: Duration, start: Percentage = getCurrentValue()) { + state = State.Fade(start, end, duration) + } + + fun sine(minimum: Percentage, maximum: Percentage, period: Duration, start: Percentage = getCurrentValue()) { + val offset = asin((start.value - State.Sine.calculateD(minimum)) / State.Sine.calculateA(minimum, maximum)) + state = State.Sine(offset, minimum, maximum, period) + } + + fun steps(steps: List, interval: Duration, startIndex: Int = 0) { + state = State.Step(steps.toImmutableList(), interval, startIndex) + } + + fun switch(a: Percentage, b: Percentage, interval: Duration, startAtCurrent: Boolean = true) { + val startIndex = startAtCurrent + .takeIf { it } + ?.let { getCurrentValue() } + ?.takeIf { it == b } + ?.let { 1 } ?: 0 + + steps(persistentListOf(a, b), interval, startIndex) + } +} + +@OptIn(ExperimentalTime::class) +class ColorDV(initialStaticValue: Color = Color.WHITE): DynamicValue { + private sealed interface State { + data class Static(val value: Color): State + data class Fade(val start: Color, val end: Color, val duration: Duration): State { + val deltaHue = ((end.hue.degree - start.hue.degree).mod(360f) + 540f).mod(360f) - 180f + val deltaSaturation = end.saturation.value - start.saturation.value + val deltaBrightness = end.brightness.value - start.brightness.value + } + } + + private var stateChangeMark = TimeSource.Monotonic.markNow() + private var state: State = State.Static(initialStaticValue) + set(value) { + field = value + stateChangeMark = TimeSource.Monotonic.markNow() + } + + override fun getCurrentValue(): Color { + val elapsedTime = stateChangeMark.elapsedNow() + + return when(val s = state) { + is State.Static -> s.value + is State.Fade -> { + val progress = min(elapsedTime / s.duration, 1.0) + + Color( + hue = Angle((s.start.hue.degree + s.deltaHue * progress).toFloat()), + saturation = Percentage((s.start.saturation.value + s.deltaSaturation * progress).toFloat()), + brightness = Percentage((s.start.brightness.value + s.deltaBrightness * progress).toFloat()) + ) + } + } + } + + fun static(value: Color) { + state = State.Static(value) + } + + fun fade(end: Color, duration: Duration, start: Color = getCurrentValue()) { + state = State.Fade(start, end, duration) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Runner.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Runner.kt new file mode 100644 index 0000000..e4e9407 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Runner.kt @@ -0,0 +1,59 @@ +package de.moritzruth.theaterdsl + +import de.moritzruth.theaterdsl.dmx.EnttecOpenDmxUsb +import de.moritzruth.theaterdsl.dmx.PerDeviceDmxDataWriter +import kotlinx.coroutines.* +import kotlin.math.roundToLong +import kotlin.system.exitProcess +import kotlin.system.measureTimeMillis +import kotlin.time.Duration + +suspend fun runShow(devices: Set, acts: List) = coroutineScope { + launch { + while(isActive) { + val took = measureTimeMillis { + val data = UByteArray(512) + val writer = PerDeviceDmxDataWriter(data) + + for (device in devices) { + writer.reset(0, device.firstChannel, device.numberOfChannels) + device.writeDmxData(writer) + } + + EnttecOpenDmxUsb.data = data + } + + delay(((1000.0 / 30) - took).roundToLong()) + } + } + + var lastStepJob: Job? = null + + for (act in acts) { + println("ACT: ${act.name}\n") + + for (scene in act.scenes) { + println("SCENE: ${scene.name}") + + for (step in scene.steps) { + println("NEXT STEP: ${step.trigger.format()}") + val input = readln() + if (input === "q") exitProcess(0) + + lastStepJob?.cancelAndJoin() + lastStepJob = launch(SupervisorJob()) { + val context = object : StepRunContext, CoroutineScope by this { + + } + + with(step) { + runner?.let { context.it() } + } + } + } + } + } + + println("\nEND OF THE SHOW") + delay(Duration.INFINITE) +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Scene.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Scene.kt new file mode 100644 index 0000000..63f1b82 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Scene.kt @@ -0,0 +1,5 @@ +package de.moritzruth.theaterdsl + +import kotlinx.collections.immutable.ImmutableList + +class Scene(val name: String, val steps: ImmutableList) \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Step.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Step.kt new file mode 100644 index 0000000..6e50623 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Step.kt @@ -0,0 +1,42 @@ +package de.moritzruth.theaterdsl + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.coroutines.CoroutineScope + +enum class PropPosition { + PROSCENIUM_LEFT, + PROSCENIUM_CENTER, + PROSCENIUM_RIGHT, + LEFT, + CENTER, + RIGHT, + BACKDROP +} + +@TheaterDslMarker +interface StepRunContext: CoroutineScope + +typealias StepRunner = (StepRunContext.() -> Unit) + +/** + * A string which has optional additional details separated by " / " + */ +@JvmInline +value class StringWithDetails(val value: String) { + companion object { + const val DELIMITER = " / " + } + + val main get() = value.split(DELIMITER)[0] + val details: String? get() = value.split(DELIMITER, limit = 2).takeIf { it.size == 2 }?.let { it[1] } + val hasDetails get() = details !== null +} + +class Step( + val trigger: StepTrigger, + val actorEntrances: ImmutableSet, + val actorExits: ImmutableSet, + val actorsOnStage: ImmutableList, + val runner: StepRunner? +) diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/StepTrigger.kt b/src/main/kotlin/de/moritzruth/theaterdsl/StepTrigger.kt new file mode 100644 index 0000000..5c2e3d8 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/StepTrigger.kt @@ -0,0 +1,37 @@ +package de.moritzruth.theaterdsl + +import kotlin.time.Duration + +sealed interface StepTrigger { + fun format(): String + + data class MusicStart(val title: String, val duration: Duration): StepTrigger { + override fun format() = "music start: $title" + } + + object MusicEnd: StepTrigger { + override fun format() = "music end" + } + + data class Curtain(val state: State, val whileMoving: Boolean): StepTrigger { + enum class State { + OPEN, + CLOSED + } + + override fun format() = "curtain: $state${if (whileMoving) "(while moving)" else ""}" + } + + data class Light(val state: State, val whileFading: Boolean): StepTrigger { + enum class State { + ON, + OFF + } + + override fun format() = "light: $state${if (whileFading) "(while fading)" else ""}" + } + + data class Custom(val text: String): StepTrigger { + override fun format() = "custom: $text" + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/Warnings.kt b/src/main/kotlin/de/moritzruth/theaterdsl/Warnings.kt new file mode 100644 index 0000000..0530ac2 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/Warnings.kt @@ -0,0 +1,5 @@ +package de.moritzruth.theaterdsl + +inline fun runOptionalSanityChecks(block: () -> Unit) { + block() +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxAddress.kt b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxAddress.kt new file mode 100644 index 0000000..392fa8f --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxAddress.kt @@ -0,0 +1,17 @@ +package de.moritzruth.theaterdsl.dmx + +@JvmInline +/** + * A **one-based** (with counting starting at 1) DMX address. + */ +value class DmxAddress(val value: UInt) : Comparable { + companion object { + val VALUE_RANGE = 1u..512u + } + + init { + require(value in VALUE_RANGE) { "must be in 1..512" } + } + + override fun compareTo(other: UInt): Int = value.compareTo(other) +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxDataWriter.kt b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxDataWriter.kt new file mode 100644 index 0000000..9592bdd --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxDataWriter.kt @@ -0,0 +1,15 @@ +package de.moritzruth.theaterdsl.dmx + +import de.moritzruth.theaterdsl.value.Percentage + +interface DmxDataWriter { + fun writeRaw(value: DmxValue) + + fun writePercentage(value: Percentage) = writeRaw(value.roundToDmxValue()) + + /** + * @param startAtOne Whether the written value is in `1..255` or in `0..255`. + */ + fun writeInRange(range: ClosedFloatingPointRange, value: Float, startAtOne: Boolean = false) = + writeRaw(Percentage((value - range.start) / (range.endInclusive - range.start)).roundToDmxValue(startAtOne)) +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxValue.kt b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxValue.kt new file mode 100644 index 0000000..8b00999 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/DmxValue.kt @@ -0,0 +1,20 @@ +package de.moritzruth.theaterdsl.dmx + +import de.moritzruth.theaterdsl.value.Percentage +import kotlin.math.roundToInt + +@JvmInline +value class DmxValue(val value: UByte) : Comparable { + companion object { + val VALUE_RANGE = 0u..255u + } + + override fun compareTo(other: UByte): Int = value.compareTo(other) +} + +/** + * @param startAtOne Whether the output range is `1..255` or `0..255`. + */ +fun Percentage.roundToDmxValue(startAtOne: Boolean = false): DmxValue = + if (startAtOne) DmxValue(((value * (DmxValue.VALUE_RANGE.last.toFloat() - 1f)).roundToInt() + 1).toUByte()) + else DmxValue((value * DmxValue.VALUE_RANGE.last.toFloat()).roundToInt().toUByte()) diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/dmx/EnttecOpenDmxUsb.kt b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/EnttecOpenDmxUsb.kt new file mode 100644 index 0000000..2084a82 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/EnttecOpenDmxUsb.kt @@ -0,0 +1,89 @@ +package de.moritzruth.theaterdsl.dmx + +import com.fazecast.jSerialComm.SerialPort +import com.fazecast.jSerialComm.SerialPortIOException +import kotlin.concurrent.thread + +object EnttecOpenDmxUsb { + private const val PORT_NAME = "FT232R USB UART" + + var data = UByteArray(512) + set(value) { + require(value.size <= 512) { "array length <= 512" } + field = value + } + + private val thread: Thread = thread( + isDaemon = true, + name = "DMX sender", + priority = Thread.NORM_PRIORITY - 1, + start = false + ) { + while (Thread.currentThread().isAlive) { + tryConnection() + Thread.sleep(1000) + } + } + + fun start() { + thread.start() + } + + private fun tryConnection() { + val port = SerialPort.getCommPorts().find { it.portDescription == PORT_NAME } + + if (port !== null) { + val isSuccess = setupPort(port) + if (isSuccess) { + loopSend(port) + } + } + } + + fun loopSend(port: SerialPort) { + Thread.currentThread().priority = Thread.NORM_PRIORITY + 1 + val stream = port.outputStream + + try { + while (Thread.currentThread().isAlive) { + if (!port.isOpen) return + + port.clearRTS() + port.setDTR() + port.setBreak() + Thread.sleep(0, 92000) + port.clearRTS() + port.setDTR() + port.clearBreak() + Thread.sleep(0, 12000) + stream.write(0) + stream.write(data.toByteArray()) + stream.flush() + + Thread.sleep(23) + } + } catch (exception: SerialPortIOException) { + exception.printStackTrace() + port.closePort() + } + } + + fun setupPort(port: SerialPort): Boolean { + if (port.openPort()) { + port.baudRate = 250000 + port.clearRTS() + port.setDTR() + port.numDataBits = 8 + port.parity = SerialPort.NO_PARITY + port.numStopBits = SerialPort.TWO_STOP_BITS + port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED) + + println("DMX interface connected") + return true + } else { + if (port.lastErrorCode == 13) System.err.println("The serial port could not be opened because of insufficient permissions (13).") + else System.err.println("The serial port could not be opened (${port.lastErrorCode}).") + return false + } + } +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/dmx/PerDeviceDmxDataWriter.kt b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/PerDeviceDmxDataWriter.kt new file mode 100644 index 0000000..640e638 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/dmx/PerDeviceDmxDataWriter.kt @@ -0,0 +1,19 @@ +package de.moritzruth.theaterdsl.dmx + +class PerDeviceDmxDataWriter(val data: UByteArray): DmxDataWriter { + private var nextIndex = 0 + private var remainingWrites = 0u + + override fun writeRaw(value: DmxValue) { + if (remainingWrites == 0u) throw IndexOutOfBoundsException(nextIndex) + data[nextIndex] = value.value + nextIndex += 1 + remainingWrites -= 1u + } + + fun reset(universe: Int, firstAddress: DmxAddress, numberOfChannels: UInt) { + nextIndex = + (universe * DmxAddress.VALUE_RANGE.last.toInt()) + firstAddress.value.toInt() - 1 // DMX addresses are one-based, array indices are zero-based + remainingWrites = numberOfChannels + } +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Angle.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Angle.kt new file mode 100644 index 0000000..8662062 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Angle.kt @@ -0,0 +1,34 @@ +package de.moritzruth.theaterdsl.value + +@JvmInline +value class Angle(val degree: Float) : Comparable { + override fun toString(): String = "$degree°" + + override fun compareTo(other: Angle): Int = degree.compareTo(other.degree) + + fun inSingleRotation(): Angle = Angle( + if (degree == 360f || degree == -360f) degree + else degree.mod(360f) + ) + + class Range( + override val start: Angle, + override val endInclusive: Angle + ): ClosedFloatingPointRange { + override fun lessThanOrEquals(a: Angle, b: Angle): Boolean = a.degree <= b.degree + + fun asFloatRange(): ClosedFloatingPointRange = start.degree..endInclusive.degree + + companion object { + val FULL_ROTATION: Range = Range(Angle(0f), Angle(360f)) + } + } +} + +inline val Int.degrees: Angle + get() = Angle(this.toFloat()) + +inline val Double.degrees: Angle + get() = Angle(this.toFloat()) + +fun Float.asAngle(): Angle = Angle(this) diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Color.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Color.kt new file mode 100644 index 0000000..e020887 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Color.kt @@ -0,0 +1,60 @@ +package de.moritzruth.theaterdsl.value + +import kotlin.math.abs + +data class Color(val hue: Angle, val saturation: Percentage, val brightness: Percentage) { + fun getRGB(): Triple { + val c = brightness.value * saturation.value + val h = hue.degree / 60f + val x = c * (1 - abs(h.mod(2f) - 1)) + val m = brightness.value - c + + val r: Float + val g: Float + val b: Float + + when { + h < 1 -> { + r = c + g = x + b = 0f + } + + h < 2 -> { + r = x + g = c + b = 0f + } + + h < 3 -> { + r = 0f + g = c + b = x + } + + h < 4 -> { + r = 0f + g = x + b = c + } + + h < 5 -> { + r = x + g = 0f + b = c + } + + else /* h < 6 */ -> { + r = c + g = 0f + b = x + } + } + + return Triple(Percentage(r + m), Percentage(g + m), Percentage(b + m)) + } + + companion object { + val WHITE = Color(0.degrees, 0.percent, 100.percent) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Frequency.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Frequency.kt new file mode 100644 index 0000000..9f5d681 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Frequency.kt @@ -0,0 +1,28 @@ +package de.moritzruth.theaterdsl.value + +@JvmInline +value class Frequency(val hertz: Float) : Comparable { + companion object { + val ZERO: Frequency = Frequency(0f) + val INFINITY: Frequency = Frequency(Float.POSITIVE_INFINITY) + } + + init { + require(hertz >= 0) { "must be at least 0" } + } + + override fun toString(): String = "${hertz}hz" + + override fun compareTo(other: Frequency): Int = hertz.compareTo(other.hertz) + + class Range( + override val start: Frequency, + override val endInclusive: Frequency + ) : ClosedFloatingPointRange { + override fun lessThanOrEquals(a: Frequency, b: Frequency): Boolean = a.hertz <= b.hertz + + fun asFloatRange(): ClosedFloatingPointRange = start.hertz..endInclusive.hertz + + override fun toString(): String = "$start..$endInclusive" + } +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Math.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Math.kt new file mode 100644 index 0000000..4932965 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Math.kt @@ -0,0 +1,18 @@ +package de.moritzruth.theaterdsl.value + +import kotlin.math.pow +import kotlin.math.roundToInt + +val IntProgression.delta: Int get() = last - first +val ClosedFloatingPointRange.delta: Float get() = endInclusive - start + +fun Int.transfer(from: IntRange, to: IntRange): Int = + ((this - from.first) / from.delta) * to.delta + to.first + +fun Float.transfer(from: ClosedFloatingPointRange, to: ClosedFloatingPointRange): Float = + ((this - from.start) / from.delta) * to.delta + to.start + +fun Float.toString(decimalPlaces: Int): String { + val s = (this * (10f.pow(decimalPlaces))).roundToInt().toString().padStart(decimalPlaces + 1, '0') + return s.dropLast(decimalPlaces) + "." + s.takeLast(decimalPlaces) +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Percentage.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Percentage.kt new file mode 100644 index 0000000..314cc56 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Percentage.kt @@ -0,0 +1,47 @@ +package de.moritzruth.theaterdsl.value + +@JvmInline +value class Percentage(val value: Float) : Comparable { + companion object { + val VALUE_RANGE: ClosedFloatingPointRange = 0f..1f + } + + init { + require(value in VALUE_RANGE) { "value ($value) must be in 0..1" } + } + + fun ofRange(range: ClosedFloatingPointRange): Float = value.transfer(VALUE_RANGE, range) + + override fun compareTo(other: Percentage): Int = value.compareTo(other.value) + + override fun toString(): String = "${value * 100}%" + + class Range( + override val start: Percentage, + override val endInclusive: Percentage + ) : ClosedFloatingPointRange { + override fun lessThanOrEquals(a: Percentage, b: Percentage): Boolean = a.value <= b.value + + fun asFloatRange(): ClosedFloatingPointRange = start.value..endInclusive.value + + companion object { + val FULL: Range = Range(VALUE_RANGE.start.asPercentage(), VALUE_RANGE.endInclusive.asPercentage()) + } + } +} + +inline val Int.percent: Percentage + get() = try { + Percentage(this / 100f) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("must be in 0..100") + } + +inline val Double.percent: Percentage + get() = try { + Percentage(this.toFloat() / 100f) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("must be in 0..100") + } + +fun Float.asPercentage(): Percentage = Percentage(this) diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/RangesAndValues.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/RangesAndValues.kt new file mode 100644 index 0000000..eaae50d --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/RangesAndValues.kt @@ -0,0 +1,75 @@ +package de.moritzruth.theaterdsl.value + +import kotlinx.collections.immutable.ImmutableSet +import kotlin.math.abs + +data class FloatRangesAndValues(val ranges: ImmutableSet>, val values: ImmutableSet) { + init { + require(ranges.isNotEmpty() || values.isNotEmpty()) { "at least one range or value is required" } + } + + val highest: Float + val highestBeforeInfinity: Float + val lowest: Float + val lowestAfterInfinity: Float + + init { + var l = Float.POSITIVE_INFINITY + var lai = Float.POSITIVE_INFINITY + var h = Float.NEGATIVE_INFINITY + var hbi = Float.NEGATIVE_INFINITY + + for (v in values + ranges.flatMap { listOf(it.start, it.endInclusive) }) { + if (v < lai) { + l = v + if (v != Float.NEGATIVE_INFINITY) lai = v + } + + if (v > hbi) { + h = v + if (v != Float.POSITIVE_INFINITY) hbi = v + } + } + + highest = h + highestBeforeInfinity = hbi + lowest = l + lowestAfterInfinity = lai + } + + fun getNearestTo(value: Float): Float { + var nearestValue = 0f + var nearestDistance = 0f + var isFirst = true + + fun update(v: Float) { + if (isFirst) { + nearestValue = v + nearestDistance = v - value + isFirst = false + return + } + + val distance = abs(v - value) + if (distance < nearestDistance) { + nearestValue = v + nearestDistance = distance + } + } + + for (v in values) { + if (v == value) return v + update(v) + } + + for (range in ranges) { + if (value < range.start) update(range.start) + else if (value > range.endInclusive) update(range.endInclusive) + else return value + } + + return nearestValue + } + + operator fun contains(value: Float): Boolean = getNearestTo(value) == value +} diff --git a/src/main/kotlin/de/moritzruth/theaterdsl/value/Temperature.kt b/src/main/kotlin/de/moritzruth/theaterdsl/value/Temperature.kt new file mode 100644 index 0000000..f2ae0e0 --- /dev/null +++ b/src/main/kotlin/de/moritzruth/theaterdsl/value/Temperature.kt @@ -0,0 +1,8 @@ +package de.moritzruth.theaterdsl.value + +@JvmInline +value class Temperature(val kelvin: Float) : Comparable { + override fun toString(): String = "${kelvin}K" + + override fun compareTo(other: Temperature): Int = kelvin.compareTo(other.kelvin) +} diff --git a/versions.properties b/versions.properties new file mode 100644 index 0000000..c08f3f6 --- /dev/null +++ b/versions.properties @@ -0,0 +1,24 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.51.0 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file + +version.com.fazecast..jSerialComm=2.9.3 + +version.io.github.oshai..kotlin-logging-jvm=4.0.0-beta-28 + +version.kotlin=1.8.20 + +version.kotlinx.collections.immutable=0.3.5 + +version.kotlinx.coroutines=1.7.1 + +version.kotlinx.datetime=0.4.0 + +version.kotlinx.serialization=1.5.1 + +version.org.slf4j..slf4j-simple=2.0.7