Initial commit
commit
037306a7a0
@ -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
|
Binary file not shown.
@ -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)
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue