commit
037306a7a0
39 changed files with 1473 additions and 0 deletions
-
8.drone.yml
-
3.gitignore
-
10build.gradle
-
5examples/graph-definition.xml
-
3gradle.properties
-
BINgradle/wrapper/gradle-wrapper.jar
-
6gradle/wrapper/gradle-wrapper.properties
-
183gradlew
-
100gradlew.bat
-
39script/create-swizzle.kts
-
5settings.gradle
-
18threedom-kapt/build.gradle
-
4threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventName.kt
-
98threedom-kapt/src/main/kotlin/me/eater/threedom/kapt/EventNameProcessor.kt
-
1threedom-kapt/src/main/resources/META-INF/gradle/incremental.annotation.processors
-
1threedom-kapt/src/main/resources/META-INF/services/javax.annotation.processing.Processor
-
37threedom/build.gradle
-
125threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt
-
80threedom/src/main/kotlin/me/eater/threedom/dom/EventTree.kt
-
13threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt
-
41threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt
-
12threedom/src/main/kotlin/me/eater/threedom/dom/INodeContainer.kt
-
10threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt
-
177threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt
-
7threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt
-
18threedom/src/main/kotlin/me/eater/threedom/dom/event/DOMTreeUpdate.kt
-
10threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt
-
5threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeIDUpdate.kt
-
5threedom/src/main/kotlin/me/eater/threedom/dom/query/NodeQuery.kt
-
25threedom/src/main/kotlin/me/eater/threedom/event/Event.kt
-
5threedom/src/main/kotlin/me/eater/threedom/event/EventControl.kt
-
15threedom/src/main/kotlin/me/eater/threedom/event/EventDispatcher.kt
-
7threedom/src/main/kotlin/me/eater/threedom/event/EventListener.kt
-
45threedom/src/main/kotlin/me/eater/threedom/utils/MutableOrderedSetListIterator.kt
-
85threedom/src/main/kotlin/me/eater/threedom/utils/ObservableSet.kt
-
95threedom/src/main/kotlin/me/eater/threedom/utils/OrderedSet.kt
-
12threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt
-
134threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt
-
26threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt
@ -0,0 +1,8 @@ |
|||
--- |
|||
kind: pipeline |
|||
name: build |
|||
|
|||
steps: |
|||
- name: build |
|||
image: d.xr.to/jdk |
|||
command: ./gradle build |
@ -0,0 +1,3 @@ |
|||
out |
|||
build |
|||
.gradle |
@ -0,0 +1,10 @@ |
|||
group 'me.eater.threedom' |
|||
version '1.0-SNAPSHOT' |
|||
|
|||
allprojects { |
|||
repositories { |
|||
jcenter() |
|||
mavenCentral() |
|||
maven { url 'https://jitpack.io' } |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
<graph> |
|||
<box diameter="10" position="[33, 40, 3]"> |
|||
|
|||
</box> |
|||
</graph> |
@ -0,0 +1,3 @@ |
|||
kotlin.code.style=official |
|||
# kapt.incremental.apt=true |
|||
# kapt.use.worker.api=true |
@ -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 |
@ -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" "$@" |
@ -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 |
@ -0,0 +1,39 @@ |
|||
val items = listOf("xyzw", "rgba", "stpq").map { it.toCharArray() } |
|||
|
|||
|
|||
val map = mutableMapOf<Int, MutableSet<List<Int>>>() |
|||
|
|||
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" |
|||
) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
rootProject.name = 'threedom' |
|||
|
|||
include 'threedom-kapt' |
|||
include 'threedom' |
|||
|
@ -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" |
|||
} |
@ -0,0 +1,4 @@ |
|||
package me.eater.threedom.kapt |
|||
|
|||
@Target(AnnotationTarget.CLASS) |
|||
annotation class EventName(val eventName: String) |
@ -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<out TypeElement>?, 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<String, String>() |
|||
|
|||
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<String>("eventClass") |
|||
returnType<String>() |
|||
|
|||
body { |
|||
append("return EVENT_MAPPING[eventClass] ?: eventClass") |
|||
} |
|||
} |
|||
|
|||
function( |
|||
"getEventName", INLINE |
|||
) { |
|||
typeParam("reified T") |
|||
returnType<String>() |
|||
|
|||
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 |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
me.eater.threedom.kapt.EventNameProcessor,dynamic |
@ -0,0 +1 @@ |
|||
me.eater.threedom.kapt.EventNameProcessor |
@ -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" |
|||
} |
@ -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<DOMTreeUpdate.Remove> { (event) -> |
|||
removeNodeFromSearch(event.child) |
|||
} |
|||
|
|||
on<DOMTreeUpdate.Insert> { (event) -> |
|||
addNodeToSearch(event.child) |
|||
} |
|||
|
|||
on<NodeIDUpdate> { (event) -> |
|||
event.old?.let { |
|||
byId.remove(it, event.node.nodeId) |
|||
} |
|||
|
|||
event.new?.let { |
|||
byId.putIfAbsent(it, event.node.nodeId) |
|||
} |
|||
} |
|||
|
|||
on<NodeClassListUpdate.Added> { (event) -> |
|||
for (className in event.classNames) { |
|||
byClass.getOrPut(className, ::mutableSetOf).add(event.node.nodeId) |
|||
} |
|||
} |
|||
|
|||
on<NodeClassListUpdate.Removed> { (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<Long, INode<*>> = mutableMapOf() |
|||
private val byId: MutableMap<String, Long> = mutableMapOf() |
|||
private val byClass: MutableMap<String, MutableSet<Long>> = mutableMapOf() |
|||
|
|||
override fun <T> addEventListener(eventName: String, refNode: INode<*>, block: (Event<T>) -> Unit) { |
|||
@Suppress("UNCHECKED_CAST") |
|||
eventTree.addEventListener(eventName, refNode, block as (Event<*>) -> Unit) |
|||
} |
|||
|
|||
fun <T> addEventListener(eventName: String, block: (Event<T>) -> Unit) = addEventListener(eventName, root, block) |
|||
|
|||
fun <T> addTopLevelEventListener(eventName: String, block: (Event<T>) -> 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<INode<*>> = root.sequence() |
|||
override fun iterator(): Iterator<INode<*>> = root.iterator() |
|||
|
|||
override fun getNodeById(id: String): INode<*>? = byId[id]?.let(allNodes::get) |
|||
|
|||
override fun getNodesByClassName(className: String): Sequence<INode<*>> = |
|||
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 <reified T> on(topLevel: Boolean = false, noinline block: (Event<T>) -> Unit) = if (topLevel) |
|||
addTopLevelEventListener(EventNames.getEventName<T>(), block) |
|||
else |
|||
addEventListener(EventNames.getEventName<T>(), block) |
|||
|
|||
override fun <T : INode<T>> createNode(nodeType: KClass<T>): T = |
|||
nodeType.java.getConstructor(IDocument::class.java).newInstance(this) |
|||
} |
|||
|
|||
|
@ -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<String, MutableSet<(Event<*>) -> Unit>> = |
|||
mutableMapOf() |
|||
private val listeners: MutableMap<String, MutableMap<Long, MutableSet<(Event<*>) -> Unit>>> = |
|||
mutableMapOf() |
|||
private val nodes: MutableMap<Long, MutableSet<String>> = 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 <reified T> on(target: INode<*>, noinline block: (Event<T>) -> Unit) = |
|||
addEventListener(EventNames.getEventName<T>(), target, block as (Event<*>) -> Unit) |
|||
|
|||
@Suppress("UNCHECKED_CAST") |
|||
inline fun <reified T> on(noinline block: (Event<T>) -> Unit) = |
|||
addTopLevelEventListener(EventNames.getEventName<T>(), block as (Event<*>) -> Unit) |
|||
|
|||
} |
@ -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 <T : INode<T>> createNode(nodeType: KClass<T>): T |
|||
fun deleteNode(refNode: INode<*>) |
|||
fun <T> addEventListener(eventName: String, refNode: INode<*>, block: (Event<T>) -> Unit) |
|||
} |
|||
|
|||
inline fun <reified T : INode<T>> IDocument.createNode() = createNode(T::class) |
@ -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<T : INode<T>> : Comparable<INode<*>>, INodeContainer { |
|||
var id: String? |
|||
val classList: MutableSet<String> |
|||
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() |
|||
} |
|||
} |
@ -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<INode<*>> |
|||
|
|||
operator fun iterator(): Iterator<INode<*>> |
|||
} |
@ -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<INode<*>> |
|||
fun find(query: NodeQuery): Sequence<INode<*>> = emptySequence() |
|||
fun findOne(query: NodeQuery): INode<*>? = find(query).firstOrNull() |
|||
} |
@ -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<T : INode<T>>(document: IDocument?) : INode<T>, EventListener { |
|||
override var document: IDocument? = document |
|||
protected set |
|||
override val nodeId = INode.getNextNodeId() |
|||
private val nodes: MutableSet<INode<*>> = 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<String> = 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 <reified T> trigger(event: T) { |
|||
if (parentNode !== null) { |
|||
document?.trigger(Event(event, this)) |
|||
} |
|||
} |
|||
|
|||
override var model: Matrix4d = Matrix4d() |
|||
|
|||
var children: List<INode<*>> = 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<INode<*>> = nodes.asSequence() |
|||
override fun iterator(): Iterator<INode<*>> = nodes.iterator() |
|||
|
|||
fun recursiveSequence(): Sequence<INode<*>> = sequence { |
|||
val iterators = mutableListOf<Iterator<INode<*>>>() |
|||
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<DOMTreeUpdate>(this, ev) |
|||
if (ev.bubble) { |
|||
document?.trigger<DOMTreeUpdate>(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 <T> on(eventName: String, block: (Event<T>) -> 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<INode<*>> = if (nodes.isEmpty()) |
|||
emptySequence() |
|||
else |
|||
document?.getNodesByClassName(className)?.filter { it.hasParent(this) } ?: emptySequence() |
|||
} |
@ -0,0 +1,7 @@ |
|||
package me.eater.threedom.dom |
|||
|
|||
class PlainNode(document: IDocument?) : Node<PlainNode>(document) { |
|||
override fun cloneSelf(): PlainNode { |
|||
return PlainNode(document) |
|||
} |
|||
} |
@ -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() |
|||
} |
@ -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<String>, override val node: INode<*>): NodeClassListUpdate() |
|||
class Added(val classNames: Set<String>, override val node: INode<*>): NodeClassListUpdate() |
|||
} |
@ -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?) |
@ -0,0 +1,5 @@ |
|||
package me.eater.threedom.dom.query |
|||
|
|||
class NodeQuery() { |
|||
|
|||
} |
@ -0,0 +1,25 @@ |
|||
package me.eater.threedom.event |
|||
|
|||
import me.eater.threedom.dom.INode |
|||
|
|||
class Event<out T>(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 |
|||
} |
|||
|
|||
|
@ -0,0 +1,5 @@ |
|||
package me.eater.threedom.event |
|||
|
|||
interface EventControl { |
|||
|
|||
} |
@ -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 <reified T> EventDispatcher.trigger(event: Event<T>) = |
|||
trigger(EventNames.getEventName<T>(), event) |
|||
|
|||
inline fun <reified T> EventDispatcher.trigger(target: INode<*>, event: Event<T>) = |
|||
trigger(EventNames.getEventName<T>(), target, event) |
@ -0,0 +1,7 @@ |
|||
package me.eater.threedom.event |
|||
|
|||
interface EventListener { |
|||
fun <T> on(eventName: String, block: (Event<T>) -> Unit) |
|||
} |
|||
|
|||
inline fun <reified T> EventListener.on(noinline block: (Event<T>) -> Unit) = on(T::class.java.name, block) |
@ -0,0 +1,45 @@ |
|||
package me.eater.threedom.utils |
|||
|
|||
class MutableOrderedSetListIterator<T>(private val collection: OrderedSet<T>, private var index: Int = 0) : |
|||
MutableListIterator<T> { |
|||
|
|||
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 |
|||
} |
|||
} |
@ -0,0 +1,85 @@ |
|||
package me.eater.threedom.utils |
|||
|
|||
class ObservableSet<T>(private val onChange: (Changed<T>) -> Unit) : MutableSet<T> { |
|||
private val storage: MutableSet<T> = mutableSetOf() |
|||
|
|||
data class Changed<T>(val action: Action, val elements: Set<T>, val set: ObservableSet<T>) |
|||
|
|||
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<T>): 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<T> = 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<T>): 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<T>): Boolean { |
|||
val temp = mutableSetOf<T>() |
|||
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<T>) = storage.containsAll(elements) |
|||
|
|||
override fun isEmpty(): Boolean = storage.isEmpty() |
|||
} |
@ -0,0 +1,95 @@ |
|||
package me.eater.threedom.utils |
|||
|
|||
import java.util.* |
|||
|
|||
class OrderedSet<T>() : MutableSet<T>, MutableList<T> { |
|||
override val size |
|||
get() = items.size |
|||
|
|||
private val set: MutableSet<T> = mutableSetOf() |
|||
private val items: MutableList<T> = mutableListOf() |
|||
|
|||
constructor(elements: Collection<T>) : this() { |
|||
addAll(elements) |
|||
} |
|||
|
|||
override fun contains(element: T) = set.contains(element) |
|||
override fun containsAll(elements: Collection<T>) = 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<T> = set.spliterator() |
|||
override fun indexOf(element: T): Int = items.indexOf(element) |
|||
override fun lastIndexOf(element: T): Int = indexOf(element) |
|||
override fun listIterator(): MutableOrderedSetListIterator<T> = MutableOrderedSetListIterator(this) |
|||
override fun listIterator(index: Int): MutableOrderedSetListIterator<T> = MutableOrderedSetListIterator(this, index) |
|||
override fun subList(fromIndex: Int, toIndex: Int): OrderedSet<T> = 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<T>): 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<T>): Boolean { |
|||
return elements.map(::remove).any() |
|||
} |
|||
|
|||
override fun retainAll(elements: Collection<T>): 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<T>): 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 |
|||
} |
|||
} |
@ -0,0 +1,12 @@ |
|||
package me.eater.threedom.utils.joml |
|||
|
|||
import org.joml.Matrix4d |
|||
import org.joml.Vector3d |
|||
|
|||
fun <T : Number> 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 <T : Number> Vector3d(x: T, y: T, z: T) = Vector3d(x.toDouble(), y.toDouble(), z.toDouble()) |
@ -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<DOMTreeUpdate.Insert> { |
|||
triggered++ |
|||
} |
|||
|
|||
document.on<DOMTreeUpdate> { |
|||
triggered++ |
|||
} |
|||
|
|||
val newNode = document.createNode<PlainNode>() |
|||
document.addNode(newNode) |
|||
|
|||
triggered shouldBe 2 |
|||
} |
|||
|
|||
"test move event" { |
|||
val document = Document() |
|||
|
|||
|
|||
val newNode = document.createNode<PlainNode>() |
|||
document.addNode(newNode) |
|||
val secondNode = document.createNode<PlainNode>() |
|||
document.addNode(secondNode) |
|||
var triggered = 0 |
|||
|
|||
document.on<DOMTreeUpdate.Move> { (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<DOMTreeUpdate.Remove> { |
|||
triggered++ |
|||
} |
|||
|
|||
document.on<DOMTreeUpdate> { |
|||
triggered++ |
|||
} |
|||
|
|||
val newNode = document.createNode<PlainNode>() |
|||
document.addNode(newNode) |
|||
document.removeNode(newNode) |
|||
|
|||
triggered shouldBe 3 |
|||
} |
|||
|
|||
"test byId" { |
|||
val doc = Document() |
|||
|
|||
val newNode = doc.createNode<PlainNode>() |
|||
val secondNewNode = doc.createNode<PlainNode>() |
|||
val thirdNewNode = doc.createNode<PlainNode>() |
|||
var triggered = 0 |
|||
doc.on<NodeIDUpdate> { |
|||
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<PlainNode>() |
|||
val secondNewNode = doc.createNode<PlainNode>() |
|||
val thirdNewNode = doc.createNode<PlainNode>() |
|||
var triggered = 0 |
|||
doc.on<NodeIDUpdate> { |
|||
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 |
|||
} |
|||
}) |
@ -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<PlainNode>() |
|||
doc.addNode(node) |
|||
node.model.setTranslation(10, 0, 10) |
|||
node.absolute.getTranslation() shouldBe Vector3d(10, 0, 10) |
|||
val nodeTwo = doc.createNode<PlainNode>() |
|||
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) |
|||
} |
|||
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue