commit 037306a7a0d8b40564d4c8f351df606003c2c5cc Author: eater <=@eater.me> Date: Sun May 17 13:49:57 2020 +0200 Initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a719f0b --- /dev/null +++ b/.drone.yml @@ -0,0 +1,8 @@ +--- +kind: pipeline +name: build + +steps: + - name: build + image: d.xr.to/jdk + command: ./gradle build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc5f398 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out +build +.gradle diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9c6dc58 --- /dev/null +++ b/build.gradle @@ -0,0 +1,10 @@ +group 'me.eater.threedom' +version '1.0-SNAPSHOT' + +allprojects { + repositories { + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} diff --git a/examples/graph-definition.xml b/examples/graph-definition.xml new file mode 100644 index 0000000..0ca9d9b --- /dev/null +++ b/examples/graph-definition.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..dae8b09 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +# kapt.incremental.apt=true +# kapt.use.worker.api=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f3d88b1 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..318651d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri May 15 20:32:10 CEST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@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 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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +: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/script/create-swizzle.kts b/script/create-swizzle.kts new file mode 100644 index 0000000..51c800c --- /dev/null +++ b/script/create-swizzle.kts @@ -0,0 +1,39 @@ +val items = listOf("xyzw", "rgba", "stpq").map { it.toCharArray() } + + +val map = mutableMapOf>>() + +for (firstChar in 0..3) { + for (secondChar in 0..3) { + val char2 = listOf(firstChar, secondChar) + map.getOrPut(char2.max()!!, ::mutableSetOf).add(char2) + + for (thirdChar in 0..3) { + val char3 = listOf(firstChar, secondChar, thirdChar) + map.getOrPut(char3.max()!!, ::mutableSetOf) + .add(char3) + + for (fourth in 0..3) { + val char4 = listOf(firstChar, secondChar, thirdChar, fourth) + map.getOrPut(char4.max()!!, ::mutableSetOf) + .add(char4) + } + } + } +} + +for ((dim, nrs) in map) { + if (dim != 1) { + println("Vec${maxOf(dim + 1, 2)} ->") + } + + for (item in nrs) { + + for (set in items) { + println( + " val ${item.map(set::get).joinToString("")}\n get() = KVec${item.size}(${item.map(items[0]::get) + .joinToString(",")})\n" + ) + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..07223db --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'threedom' + +include 'threedom-kapt' +include 'threedom' + diff --git a/threedom-kapt/build.gradle b/threedom-kapt/build.gradle new file mode 100644 index 0000000..e930f3e --- /dev/null +++ b/threedom-kapt/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.72' +} + +group 'me.eater.threedom' +version '1.0-SNAPSHOT' + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation 'com.github.yanex:takenoko:0.1' +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} diff --git a/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventName.kt b/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventName.kt new file mode 100644 index 0000000..7d77405 --- /dev/null +++ b/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventName.kt @@ -0,0 +1,4 @@ +package me.eater.threedom.kapt + +@Target(AnnotationTarget.CLASS) +annotation class EventName(val eventName: String) diff --git a/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventNameProcessor.kt b/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventNameProcessor.kt new file mode 100644 index 0000000..0a97a7f --- /dev/null +++ b/threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventNameProcessor.kt @@ -0,0 +1,98 @@ +package me.eater.threedom.kapt + +import org.yanex.takenoko.* +import java.io.File +import javax.annotation.processing.* +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.TypeElement +import javax.tools.Diagnostic.Kind.ERROR + +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@SupportedAnnotationTypes("me.eater.threedom.kapt.EventName") +@SupportedOptions(EventNameProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME, "org.gradle.annotation.processing.aggregating") +class EventNameProcessor : AbstractProcessor() { + companion object { + const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated" + } + + override fun process(annotations: MutableSet?, roundEnv: RoundEnvironment): Boolean { + val annotatedElements = roundEnv.getElementsAnnotatedWith(EventName::class.java) + if (annotatedElements.isEmpty()) return false + + val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ?: run { + processingEnv.messager.printMessage(ERROR, "Can't find the target directory for generated Kotlin files.") + return false + } + + val generatedKtFile = kotlinFile("me.eater.threedom.generated") { + objectDeclaration("EventNames") { + val eventNames = mutableMapOf() + + for (element in annotatedElements) { + val typeElement = element.toTypeElementOrNull() ?: continue + val eventName = typeElement.getAnnotation(EventName::class.java).eventName + + if (eventName in eventNames) { + processingEnv.messager.printMessage( + ERROR, + "Class ${eventNames[eventName]} already uses the event name '${eventName}'", + element + ) + continue + } + + eventNames[typeElement.qualifiedName.toString()] = eventName + + property(eventName) { + initializer(typeElement.qualifiedName.toString() + "::class") + } + } + + property("EVENT_MAPPING") { + initializer( + "mapOf(\n${eventNames.map { (k, v) -> " \"" + k.replace('"', '\"') + "\" to \"" + v.replace('"', '\"') + '"' } + .joinToString(",\n")}\n)" + ) + } + + function("getEventName") { + param("eventClass") + returnType() + + body { + append("return EVENT_MAPPING[eventClass] ?: eventClass") + } + } + + function( + "getEventName", INLINE + ) { + typeParam("reified T") + returnType() + + body { + append("return getEventName(T::class.java.name)") + } + } + } + } + + File("$kaptKotlinGeneratedDir/me/eater/threedom/generated", "EventNames.kt").apply { + parentFile.mkdirs() + writeText(generatedKtFile.accept(PrettyPrinter(PrettyPrinterConfiguration()))) + } + + return true + } + + + fun Element.toTypeElementOrNull(): TypeElement? { + if (this !is TypeElement) { + processingEnv.messager.printMessage(ERROR, "Invalid element type, class expected", this) + return null + } + + return this + } +} diff --git a/threedom-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors b/threedom-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 0000000..46e6029 --- /dev/null +++ b/threedom-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +me.eater.threedom.kapt.EventNameProcessor,dynamic diff --git a/threedom-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/threedom-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..d9ff67f --- /dev/null +++ b/threedom-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +me.eater.threedom.kapt.EventNameProcessor diff --git a/threedom/build.gradle b/threedom/build.gradle new file mode 100644 index 0000000..261bfd0 --- /dev/null +++ b/threedom/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id "org.jetbrains.kotlin.kapt" version "1.3.72" +} + +group 'me.eater.threedom' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + + +test { + useJUnitPlatform() +} + +dependencies { + kapt project(':threedom-kapt') + compileOnly project(':threedom-kapt') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + implementation 'org.joml:joml:1.9.24' + + + testImplementation 'io.kotest:kotest-runner-junit5-jvm:4.0.5' + testImplementation 'io.kotest:kotest-assertions-core-jvm:4.0.5' + testImplementation 'io.kotest:kotest-property-jvm:4.0.5' +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt new file mode 100644 index 0000000..6bd4de9 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt @@ -0,0 +1,125 @@ +package me.eater.threedom.dom + +import me.eater.threedom.dom.event.DOMTreeUpdate +import me.eater.threedom.dom.event.NodeClassListUpdate +import me.eater.threedom.dom.event.NodeIDUpdate +import me.eater.threedom.event.Event +import me.eater.threedom.generated.EventNames +import kotlin.reflect.KClass + +class Document : IDocument { + val root: PlainNode = PlainNode(this) + private val eventTree = EventTree { + on { (event) -> + removeNodeFromSearch(event.child) + } + + on { (event) -> + addNodeToSearch(event.child) + } + + on { (event) -> + event.old?.let { + byId.remove(it, event.node.nodeId) + } + + event.new?.let { + byId.putIfAbsent(it, event.node.nodeId) + } + } + + on { (event) -> + for (className in event.classNames) { + byClass.getOrPut(className, ::mutableSetOf).add(event.node.nodeId) + } + } + + on { (event) -> + for (className in event.classNames) { + val set = byClass[className] ?: continue + + set.remove(event.node.nodeId) + + if (set.size == 0) { + byClass.remove(className) + } + } + } + } + + private val allNodes: MutableMap> = mutableMapOf() + private val byId: MutableMap = mutableMapOf() + private val byClass: MutableMap> = mutableMapOf() + + override fun addEventListener(eventName: String, refNode: INode<*>, block: (Event) -> Unit) { + @Suppress("UNCHECKED_CAST") + eventTree.addEventListener(eventName, refNode, block as (Event<*>) -> Unit) + } + + fun addEventListener(eventName: String, block: (Event) -> Unit) = addEventListener(eventName, root, block) + + fun addTopLevelEventListener(eventName: String, block: (Event) -> Unit) = + @Suppress("UNCHECKED_CAST") + eventTree.addTopLevelEventListener(eventName, block as (Event<*>) -> Unit) + + override fun trigger(eventName: String, targetNode: INode<*>, event: Event<*>) = + eventTree.trigger(eventName, targetNode, event) + + + override fun addNode(newNode: INode<*>) = root.addNode(newNode) + override fun removeNode(refNode: INode<*>) = root.removeNode(refNode) + + override fun deleteNode(refNode: INode<*>) { + refNode.detachFromDocument() + eventTree.removeNode(refNode) + } + + override fun removeAll() = root.removeAll() + + override fun replaceNode(newNode: INode<*>, refNode: INode<*>): Boolean = root.replaceNode(newNode, refNode) + + override fun hasChild(refNode: INode<*>): Boolean = root.hasChild(refNode) + + override fun sequence(): Sequence> = root.sequence() + override fun iterator(): Iterator> = root.iterator() + + override fun getNodeById(id: String): INode<*>? = byId[id]?.let(allNodes::get) + + override fun getNodesByClassName(className: String): Sequence> = + byClass[className]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence() + + + private fun addNodeToSearch(node: INode<*>) { + allNodes[node.nodeId] = node + + node.id?.let { byId.putIfAbsent(it, node.nodeId) } + + for (className in node.classList) { + byClass.getOrPut(className, ::mutableSetOf).add(node.nodeId) + } + } + + private fun removeNodeFromSearch(node: INode<*>) { + allNodes.remove(node.nodeId) + + node.id?.let { byId.remove(it, node.nodeId) } + + for (className in node.classList) { + val set = byClass.get(className) ?: continue + set.remove(node.nodeId) + if (set.size == 0) { + byClass.remove(className) + } + } + } + + inline fun on(topLevel: Boolean = false, noinline block: (Event) -> Unit) = if (topLevel) + addTopLevelEventListener(EventNames.getEventName(), block) + else + addEventListener(EventNames.getEventName(), block) + + override fun > createNode(nodeType: KClass): T = + nodeType.java.getConstructor(IDocument::class.java).newInstance(this) +} + + diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/EventTree.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/EventTree.kt new file mode 100644 index 0000000..4f61ef4 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/EventTree.kt @@ -0,0 +1,80 @@ +package me.eater.threedom.dom + +import me.eater.threedom.event.Event +import me.eater.threedom.generated.EventNames + +class EventTree(block: EventTree.() -> Unit = {}) { + private val topLevel: MutableMap) -> Unit>> = + mutableMapOf() + private val listeners: MutableMap) -> Unit>>> = + mutableMapOf() + private val nodes: MutableMap> = mutableMapOf() + + init { + block(this) + } + + fun addTopLevelEventListener(eventName: String, handler: (Event<*>) -> Unit) { + this.topLevel.getOrPut(eventName, ::mutableSetOf).add(handler) + } + + fun addEventListener(eventName: String, target: INode<*>, handler: (Event<*>) -> Unit) { + this.listeners.getOrPut(eventName, ::mutableMapOf).getOrPut(target.nodeId, ::mutableSetOf).add(handler) + this.nodes.getOrPut(target.nodeId, ::mutableSetOf).add(eventName) + } + + fun removeNode(node: INode<*>) { + val events = this.nodes.remove(node.nodeId) ?: return + for (event in events) { + this.listeners[event]?.remove(node.nodeId) + } + } + + fun trigger(eventName: String, target: INode<*>, event: Event<*>) { + this.topLevel[eventName]?.let { + for (handler in it) { + handler(event) + + if (!event.propagate) { + return + } + } + + if (!event.bubble) { + return + } + } + + val map = this.listeners[eventName] ?: return + var current: INode<*>? = target + + while (current != null) { + val set = map[current.nodeId] + if (set != null) { + for (eventHandler in set) { + eventHandler(event) + + if (!event.propagate) { + break + } + } + } + + if (!event.bubble) { + break + } + + current = current.parentNode + } + } + + + @Suppress("UNCHECKED_CAST") + inline fun on(target: INode<*>, noinline block: (Event) -> Unit) = + addEventListener(EventNames.getEventName(), target, block as (Event<*>) -> Unit) + + @Suppress("UNCHECKED_CAST") + inline fun on(noinline block: (Event) -> Unit) = + addTopLevelEventListener(EventNames.getEventName(), block as (Event<*>) -> Unit) + +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt new file mode 100644 index 0000000..6afde5a --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt @@ -0,0 +1,13 @@ +package me.eater.threedom.dom + +import me.eater.threedom.event.Event +import me.eater.threedom.event.EventDispatcher +import kotlin.reflect.KClass + +interface IDocument : EventDispatcher, INodeContainer { + fun > createNode(nodeType: KClass): T + fun deleteNode(refNode: INode<*>) + fun addEventListener(eventName: String, refNode: INode<*>, block: (Event) -> Unit) +} + +inline fun > IDocument.createNode() = createNode(T::class) diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt new file mode 100644 index 0000000..5014b58 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt @@ -0,0 +1,41 @@ +package me.eater.threedom.dom + +import org.joml.Matrix4d +import org.joml.Vector3d +import java.util.concurrent.atomic.AtomicLong + +interface INode> : Comparable>, INodeContainer { + var id: String? + val classList: MutableSet + val nodeId: Long + val parentNode: INode<*>? + val document: IDocument? + val absolute: Matrix4d + get() = (parentNode?.absolute ?: Matrix4d()).mul(model) + + var model: Matrix4d + + fun clone(deep: Boolean): T + fun updateParent(refNode: INode<*>?): Boolean + fun detachFromDocument() + fun hasParent(node: INode<*>): Boolean { + var current: INode<*>? = parentNode; + + while (current != null) { + if (node.nodeId == current.nodeId) { + return true + } + + current = current.parentNode + } + + return false + } + + override fun compareTo(other: INode<*>): Int = this.nodeId.compareTo(other.nodeId) + + companion object { + private val atomicNodeId = AtomicLong(0) + fun getNextNodeId() = atomicNodeId.getAndIncrement() + } +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/INodeContainer.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeContainer.kt new file mode 100644 index 0000000..c4e58a8 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeContainer.kt @@ -0,0 +1,12 @@ +package me.eater.threedom.dom + +interface INodeContainer : INodeQueryCapable { + fun addNode(newNode: INode<*>) + fun removeNode(refNode: INode<*>) + fun removeAll() + fun replaceNode(newNode: INode<*>, refNode: INode<*>): Boolean + fun hasChild(refNode: INode<*>): Boolean + fun sequence(): Sequence> + + operator fun iterator(): Iterator> +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt new file mode 100644 index 0000000..c7c87dc --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt @@ -0,0 +1,10 @@ +package me.eater.threedom.dom + +import me.eater.threedom.dom.query.NodeQuery + +interface INodeQueryCapable { + fun getNodeById(id: String): INode<*>? + fun getNodesByClassName(className: String): Sequence> + fun find(query: NodeQuery): Sequence> = emptySequence() + fun findOne(query: NodeQuery): INode<*>? = find(query).firstOrNull() +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt new file mode 100644 index 0000000..1cadf3e --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt @@ -0,0 +1,177 @@ +package me.eater.threedom.dom + +import me.eater.threedom.dom.event.DOMTreeUpdate +import me.eater.threedom.dom.event.NodeClassListUpdate +import me.eater.threedom.dom.event.NodeIDUpdate +import me.eater.threedom.event.Event +import me.eater.threedom.event.EventListener +import me.eater.threedom.event.trigger +import me.eater.threedom.utils.ObservableSet +import org.joml.Matrix4d + +abstract class Node>(document: IDocument?) : INode, EventListener { + override var document: IDocument? = document + protected set + override val nodeId = INode.getNextNodeId() + private val nodes: MutableSet> = mutableSetOf() + override var parentNode: INode<*>? = null + protected set + + private var _id: String? = null + override var id: String? + get() = _id + set(value) { + val old = _id + _id = value + + trigger(NodeIDUpdate(this, old, value)) + } + + override val classList: MutableSet = ObservableSet { + when (it.action) { + ObservableSet.Action.Removed -> trigger(NodeClassListUpdate.Removed(it.elements, this)) + ObservableSet.Action.Added -> trigger(NodeClassListUpdate.Added(it.elements, this)) + } + } + + private inline fun trigger(event: T) { + if (parentNode !== null) { + document?.trigger(Event(event, this)) + } + } + + override var model: Matrix4d = Matrix4d() + + var children: List> = nodes.toList() + + override fun addNode(newNode: INode<*>) { + nodes.add(newNode) + if (newNode.parentNode != this) { + newNode.updateParent(this) + } + } + + override fun removeNode(refNode: INode<*>) { + nodes.remove(refNode) + if (refNode.parentNode == this) { + refNode.updateParent(null) + } + } + + override fun removeAll() { + nodes.forEach(::removeNode) + } + + override fun replaceNode(newNode: INode<*>, refNode: INode<*>): Boolean { + if (nodes.remove(refNode)) { + nodes.add(newNode) + return true + } + + return false + } + + override fun sequence(): Sequence> = nodes.asSequence() + override fun iterator(): Iterator> = nodes.iterator() + + fun recursiveSequence(): Sequence> = sequence { + val iterators = mutableListOf>>() + var current = iterator() + while (true) { + for (node in current) { + yield(node) + iterators.add(node.iterator()) + } + + current = iterators.firstOrNull() ?: return@sequence + } + } + + + protected abstract fun cloneSelf(): T + + override fun clone(deep: Boolean): T { + val cloned = cloneSelf() + + if (!deep) { + return cloned + } + + for (node in cloned) { + cloned.addNode(node.clone(true)) + } + + return cloned + } + + override fun updateParent(refNode: INode<*>?): Boolean { + val parent = parentNode + if (parent == refNode) { + return true + } + + if (refNode != null && !refNode.hasChild(this)) { + return false + } + + parentNode = refNode + parent?.removeNode(this) + + val event = when { + parent == null && refNode != null -> + DOMTreeUpdate.Insert(refNode, this).also(::trigger) + refNode == null && parent != null -> { + val data = DOMTreeUpdate.Remove(parent, this) + + // Trigger on removed as well as on the parent + // Since you can't bubble from a detached node + var ev = Event(data, this) + document?.trigger(this, ev) + if (ev.bubble) { + document?.trigger(parent, Event(data, this)) + } + + // Trigger on removed as well as on the parent + // Since you can't bubble from a detached node + ev = Event(data, this) + document?.trigger(this, ev) + if (ev.bubble) { + document?.trigger(parent, Event(data, this)) + } + + return true + } + + + refNode != null && parent != null -> + DOMTreeUpdate.Move(parent, refNode, this).also(::trigger) + else -> return true + } + + trigger(event) + + return true + } + + override fun hasChild(refNode: INode<*>) = nodes.contains(refNode) + + override fun on(eventName: String, block: (Event) -> Unit) { + document?.addEventListener(eventName, this, block) + } + + override fun detachFromDocument() { + val doc = this.document ?: return + this.document = null + doc.removeNode(this) + } + + fun finalize() { + document?.removeNode(this) + } + + override fun getNodeById(id: String): INode<*>? = document?.getNodeById(id)?.takeIf { it.hasParent(this) } + override fun getNodesByClassName(className: String): Sequence> = if (nodes.isEmpty()) + emptySequence() + else + document?.getNodesByClassName(className)?.filter { it.hasParent(this) } ?: emptySequence() +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt new file mode 100644 index 0000000..940ef80 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt @@ -0,0 +1,7 @@ +package me.eater.threedom.dom + +class PlainNode(document: IDocument?) : Node(document) { + override fun cloneSelf(): PlainNode { + return PlainNode(document) + } +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/event/DOMTreeUpdate.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/event/DOMTreeUpdate.kt new file mode 100644 index 0000000..0400f44 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/event/DOMTreeUpdate.kt @@ -0,0 +1,18 @@ +package me.eater.threedom.dom.event + +import me.eater.threedom.dom.INode +import me.eater.threedom.kapt.EventName + +@EventName("DOMTreeUpdate") +sealed class DOMTreeUpdate { + abstract val child: INode<*> + + @EventName("DOMNodeRemove") + data class Remove(val parent: INode<*>, override val child: INode<*>) : DOMTreeUpdate() + + @EventName("DOMNodeInsert") + data class Insert(val parent: INode<*>, override val child: INode<*>) : DOMTreeUpdate() + + @EventName("DOMNodeMove") + data class Move(val oldParent: INode<*>, val newParent: INode<*>, override val child: INode<*>) : DOMTreeUpdate() +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt new file mode 100644 index 0000000..6721704 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt @@ -0,0 +1,10 @@ +package me.eater.threedom.dom.event + +import me.eater.threedom.dom.INode + +sealed class NodeClassListUpdate { + abstract val node: INode<*> + + class Removed(val classNames: Set, override val node: INode<*>): NodeClassListUpdate() + class Added(val classNames: Set, override val node: INode<*>): NodeClassListUpdate() +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeIDUpdate.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeIDUpdate.kt new file mode 100644 index 0000000..a2a5b0f --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeIDUpdate.kt @@ -0,0 +1,5 @@ +package me.eater.threedom.dom.event + +import me.eater.threedom.dom.INode + +data class NodeIDUpdate(val node: INode<*>, val old: String?, val new: String?) diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/query/NodeQuery.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/query/NodeQuery.kt new file mode 100644 index 0000000..b101763 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/query/NodeQuery.kt @@ -0,0 +1,5 @@ +package me.eater.threedom.dom.query + +class NodeQuery() { + +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/event/Event.kt b/threedom/src/main/kotlin/me/eater/threedom/event/Event.kt new file mode 100644 index 0000000..3d3bb03 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/event/Event.kt @@ -0,0 +1,25 @@ +package me.eater.threedom.event + +import me.eater.threedom.dom.INode + +class Event(val data: T, val source: INode<*>) { + + var bubble = true + private set + + var propagate = true + private set + + fun stopPropagation() { + propagate = false + } + + fun stopBubbling() { + bubble = false + } + + operator fun component1(): T = data + operator fun component2(): INode<*> = source +} + + diff --git a/threedom/src/main/kotlin/me/eater/threedom/event/EventControl.kt b/threedom/src/main/kotlin/me/eater/threedom/event/EventControl.kt new file mode 100644 index 0000000..9011ed9 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/event/EventControl.kt @@ -0,0 +1,5 @@ +package me.eater.threedom.event + +interface EventControl { + +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/event/EventDispatcher.kt b/threedom/src/main/kotlin/me/eater/threedom/event/EventDispatcher.kt new file mode 100644 index 0000000..05c4d4a --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/event/EventDispatcher.kt @@ -0,0 +1,15 @@ +package me.eater.threedom.event + +import me.eater.threedom.dom.INode +import me.eater.threedom.generated.EventNames + +interface EventDispatcher { + fun trigger(eventName: String, event: Event<*>) = trigger(eventName, event.source, event) + fun trigger(eventName: String, targetNode: INode<*>, event: Event<*>) +} + +inline fun EventDispatcher.trigger(event: Event) = + trigger(EventNames.getEventName(), event) + +inline fun EventDispatcher.trigger(target: INode<*>, event: Event) = + trigger(EventNames.getEventName(), target, event) diff --git a/threedom/src/main/kotlin/me/eater/threedom/event/EventListener.kt b/threedom/src/main/kotlin/me/eater/threedom/event/EventListener.kt new file mode 100644 index 0000000..f5689e8 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/event/EventListener.kt @@ -0,0 +1,7 @@ +package me.eater.threedom.event + +interface EventListener { + fun on(eventName: String, block: (Event) -> Unit) +} + +inline fun EventListener.on(noinline block: (Event) -> Unit) = on(T::class.java.name, block) diff --git a/threedom/src/main/kotlin/me/eater/threedom/utils/MutableOrderedSetListIterator.kt b/threedom/src/main/kotlin/me/eater/threedom/utils/MutableOrderedSetListIterator.kt new file mode 100644 index 0000000..01ca322 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/utils/MutableOrderedSetListIterator.kt @@ -0,0 +1,45 @@ +package me.eater.threedom.utils + +class MutableOrderedSetListIterator(private val collection: OrderedSet, private var index: Int = 0) : + MutableListIterator { + + override fun hasPrevious() = collection.size > (index - 1) && index > 1 + + override fun nextIndex() = index + 1 + override fun previous(): T { + if (!hasPrevious()) { + throw NoSuchElementException() + } + + index = previousIndex() + return collection[index] + } + + override fun previousIndex() = index - 1 + override fun add(element: T) { + collection.add(index, element) + } + + override fun hasNext() = nextIndex() < collection.size + + override fun next(): T { + if (!hasNext()) { + throw NoSuchElementException() + } + + index = nextIndex() + return collection[index] + } + + override fun remove() { + collection.removeAt(index) + + if (index > 0) { + index -= 1 + } + } + + override fun set(element: T) { + collection[index] = element + } +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/utils/ObservableSet.kt b/threedom/src/main/kotlin/me/eater/threedom/utils/ObservableSet.kt new file mode 100644 index 0000000..b1dd464 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/utils/ObservableSet.kt @@ -0,0 +1,85 @@ +package me.eater.threedom.utils + +class ObservableSet(private val onChange: (Changed) -> Unit) : MutableSet { + private val storage: MutableSet = mutableSetOf() + + data class Changed(val action: Action, val elements: Set, val set: ObservableSet) + + enum class Action { + Removed, + Added; + } + + override fun add(element: T): Boolean { + if (storage.add(element)) { + onChange(Changed(Action.Added, setOf(element), this)) + return true + } + + return false + } + + override fun addAll(elements: Collection): Boolean { + val items = elements.filter(storage::add) + + if (items.isEmpty()) { + return false + } + + onChange(Changed(Action.Added, items.toSet(), this)) + return true + } + + override fun clear() { + if (this.isEmpty()) { + return + } + + onChange(Changed(Action.Removed, storage, this)) + storage.clear() + } + + override fun iterator(): MutableIterator = storage.iterator() + + override fun remove(element: T): Boolean { + if (storage.remove(element)) { + onChange(Changed(Action.Removed, setOf(element), this)) + return true + } + + return false + } + + override fun removeAll(elements: Collection): Boolean { + val items = elements.filter(storage::remove) + + if (items.isEmpty()) { + return false + } + + onChange(Changed(Action.Removed, items.toSet(), this)) + return true + } + + override fun retainAll(elements: Collection): Boolean { + val temp = mutableSetOf() + for (item in storage) { + if (!elements.contains(item)) { + temp.add(item) + storage.remove(item) + } + } + + onChange(Changed(Action.Removed, temp, this)) + return temp.size > 0 + } + + override val size: Int + get() = storage.size + + override fun contains(element: T) = storage.contains(element) + + override fun containsAll(elements: Collection) = storage.containsAll(elements) + + override fun isEmpty(): Boolean = storage.isEmpty() +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/utils/OrderedSet.kt b/threedom/src/main/kotlin/me/eater/threedom/utils/OrderedSet.kt new file mode 100644 index 0000000..45f1efd --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/utils/OrderedSet.kt @@ -0,0 +1,95 @@ +package me.eater.threedom.utils + +import java.util.* + +class OrderedSet() : MutableSet, MutableList { + override val size + get() = items.size + + private val set: MutableSet = mutableSetOf() + private val items: MutableList = mutableListOf() + + constructor(elements: Collection) : this() { + addAll(elements) + } + + override fun contains(element: T) = set.contains(element) + override fun containsAll(elements: Collection) = set.containsAll(elements) + override fun isEmpty() = items.isEmpty() + override fun iterator() = items.iterator() + override operator fun get(index: Int) = items[index] + override fun spliterator(): Spliterator = set.spliterator() + override fun indexOf(element: T): Int = items.indexOf(element) + override fun lastIndexOf(element: T): Int = indexOf(element) + override fun listIterator(): MutableOrderedSetListIterator = MutableOrderedSetListIterator(this) + override fun listIterator(index: Int): MutableOrderedSetListIterator = MutableOrderedSetListIterator(this, index) + override fun subList(fromIndex: Int, toIndex: Int): OrderedSet = OrderedSet(items.subList(fromIndex, toIndex)) + + override fun add(element: T): Boolean { + if (set.add(element)) { + items.add(element) + return true + } + + return false + } + + override fun addAll(elements: Collection): Boolean { + return elements.map(::add).any() + } + + override fun clear() { + set.clear() + items.clear() + } + + override fun remove(element: T): Boolean { + if (set.remove(element)) { + items.remove(element) + return true + } + + return false + } + + override fun removeAll(elements: Collection): Boolean { + return elements.map(::remove).any() + } + + override fun retainAll(elements: Collection): Boolean { + return set.toSet().map { + if (elements.contains(it)) { + false + } else { + remove(it) + } + }.any() + } + + override fun add(index: Int, element: T) { + if (set.add(element)) { + items.add(index, element) + } + } + + override fun addAll(index: Int, elements: Collection): Boolean { + return items.addAll(index, elements.filter(set::add)) + } + + override fun removeAt(index: Int): T { + set.remove(items[index]) + return items.removeAt(index) + } + + override fun set(index: Int, element: T): T { + if (!set.add(element)) { + return element + } + + val old = items[index] + items[index] = element + remove(old) + set.add(element) + return old + } +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt b/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt new file mode 100644 index 0000000..64511b5 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt @@ -0,0 +1,12 @@ +package me.eater.threedom.utils.joml + +import org.joml.Matrix4d +import org.joml.Vector3d + +fun Matrix4d.setTranslation(x: T, y: T, z: T): Matrix4d = + setTranslation(x.toDouble(), y.toDouble(), z.toDouble()) + +fun Matrix4d.getTranslation(): Vector3d = getTranslation(Vector3d()) + +@Suppress("FunctionName") +fun Vector3d(x: T, y: T, z: T) = Vector3d(x.toDouble(), y.toDouble(), z.toDouble()) diff --git a/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt b/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt new file mode 100644 index 0000000..ebda536 --- /dev/null +++ b/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt @@ -0,0 +1,134 @@ +package me.eater.test.threedom.dom + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.sequences.shouldHaveCount +import io.kotest.matchers.sequences.shouldHaveSingleElement +import io.kotest.matchers.shouldBe +import me.eater.threedom.dom.Document +import me.eater.threedom.dom.PlainNode +import me.eater.threedom.dom.createNode +import me.eater.threedom.dom.event.DOMTreeUpdate +import me.eater.threedom.dom.event.NodeIDUpdate + +class DocumentTest : StringSpec({ + "test insert event" { + val document = Document() + var triggered = 0; + document.on { + triggered++ + } + + document.on { + triggered++ + } + + val newNode = document.createNode() + document.addNode(newNode) + + triggered shouldBe 2 + } + + "test move event" { + val document = Document() + + + val newNode = document.createNode() + document.addNode(newNode) + val secondNode = document.createNode() + document.addNode(secondNode) + var triggered = 0 + + document.on { (data) -> + data.newParent shouldBe secondNode + data.oldParent shouldBe document.root + data.child shouldBe newNode + triggered++ + } + + + secondNode.addNode(newNode) + triggered shouldBe 1 + } + + "test remove event" { + val document = Document() + var triggered = 0 + document.on { + triggered++ + } + + document.on { + triggered++ + } + + val newNode = document.createNode() + document.addNode(newNode) + document.removeNode(newNode) + + triggered shouldBe 3 + } + + "test byId" { + val doc = Document() + + val newNode = doc.createNode() + val secondNewNode = doc.createNode() + val thirdNewNode = doc.createNode() + var triggered = 0 + doc.on { + triggered++ + } + + newNode.id = ":3" + doc.addNode(secondNewNode) + doc.addNode(thirdNewNode) + + doc.getNodeById(":3") shouldBe null + thirdNewNode.addNode(newNode) + doc.getNodeById(":3") shouldBe newNode + secondNewNode.getNodeById(":3") shouldBe null + thirdNewNode.getNodeById(":3") shouldBe newNode + thirdNewNode.removeAll() + thirdNewNode.getNodeById(":3") shouldBe null + doc.getNodeById(":3") shouldBe null + doc.addNode(newNode) + doc.getNodeById(":3") shouldBe newNode + newNode.id = ":/" + doc.getNodeById(":3") shouldBe null + doc.getNodeById(":/") shouldBe newNode + } + + "test byClass" { + val doc = Document() + + val newNode = doc.createNode() + val secondNewNode = doc.createNode() + val thirdNewNode = doc.createNode() + var triggered = 0 + doc.on { + triggered++ + } + + newNode.classList.add(":3") + doc.addNode(secondNewNode) + doc.addNode(thirdNewNode) + + doc.getNodesByClassName(":3") shouldHaveCount 0 + thirdNewNode.addNode(newNode) + doc.getNodesByClassName(":3") shouldHaveSingleElement newNode + secondNewNode.getNodesByClassName(":3") shouldHaveCount 0 + thirdNewNode.getNodesByClassName(":3") shouldHaveSingleElement newNode + thirdNewNode.removeAll() + thirdNewNode.getNodesByClassName(":3") shouldHaveCount 0 + doc.getNodesByClassName(":3") shouldHaveCount 0 + newNode.classList.clear() + newNode.classList.add(":/") + doc.addNode(newNode) + doc.getNodesByClassName(":3") shouldHaveCount 0 + doc.getNodesByClassName(":/") shouldHaveSingleElement newNode + newNode.classList.clear() + newNode.classList.add(":3") + doc.getNodesByClassName(":/") shouldHaveCount 0 + doc.getNodesByClassName(":3") shouldHaveSingleElement newNode + } +}) diff --git a/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt b/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt new file mode 100644 index 0000000..d2b47ff --- /dev/null +++ b/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt @@ -0,0 +1,26 @@ +package me.eater.test.threedom.dom + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import me.eater.threedom.dom.Document +import me.eater.threedom.dom.PlainNode +import me.eater.threedom.dom.createNode +import me.eater.threedom.utils.joml.Vector3d +import me.eater.threedom.utils.joml.getTranslation +import me.eater.threedom.utils.joml.setTranslation + +class PositionTest : StringSpec({ + "ensure positioning works" { + val doc = Document() + val node = doc.createNode() + doc.addNode(node) + node.model.setTranslation(10, 0, 10) + node.absolute.getTranslation() shouldBe Vector3d(10, 0, 10) + val nodeTwo = doc.createNode() + node.addNode(nodeTwo) + nodeTwo.model.setTranslation(-10, 20, 0) + nodeTwo.absolute.getTranslation() shouldBe Vector3d(0, 20, 10) + doc.addNode(nodeTwo) + nodeTwo.absolute.getTranslation() shouldBe Vector3d(-10, 20, 0) + } +})