Browse Source

add gl and example

master
eater 2 years ago
parent
commit
7fc4b44571
Signed by: eater GPG Key ID: AD2560A0F84F0759
  1. 5
      examples/graph-definition.xml
  2. 3
      settings.gradle
  3. 67
      threedom-example/build.gradle
  4. 251
      threedom-example/src/main/kotlin/me/eater/threedom/example/main.kt
  5. 60
      threedom-example/src/main/kotlin/me/eater/threedom/example/shader/Plain.kt
  6. 8
      threedom-example/src/main/kotlin/me/eater/threedom/example/vertex/Simple.kt
  7. 32
      threedom-example/src/main/resources/shaders/plain/fragment.glsl
  8. 7
      threedom-example/src/main/resources/shaders/plain/fragment_lamp.glsl
  9. 21
      threedom-example/src/main/resources/shaders/plain/vertex.glsl
  10. BIN
      threedom-example/src/main/resources/textures/fujiwara.jpg
  11. 37
      threedom-gl/build.gradle
  12. 11
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/GL.kt
  13. 32
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/Shader.kt
  14. 46
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderPreProcessor.kt
  15. 220
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderProgram.kt
  16. 10
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/GLRenderNode.kt
  17. 12
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/ICamera.kt
  18. 53
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/PerspectiveCamera.kt
  19. 49
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/TriMesh.kt
  20. 3
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderCompilationException.kt
  21. 4
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderProgramLinkingException.kt
  22. 8
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/geometry/Plane.kt
  23. 55
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/texture/Texture.kt
  24. 78
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexArrayObject.kt
  25. 45
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBuffer.kt
  26. 3
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBufferMutable.kt
  27. 56
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexData.kt
  28. 73
      threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexDataPoint.kt
  29. 10
      threedom-kapt/build.gradle
  30. 10
      threedom/build.gradle
  31. 102
      threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt
  32. 8
      threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt
  33. 13
      threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt
  34. 16
      threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt
  35. 47
      threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt
  36. 4
      threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt
  37. 4
      threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt
  38. 7
      threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeLocationIndexStateChange.kt
  39. 10
      threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderNode.kt
  40. 5
      threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderTarget.kt
  41. 61
      threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt
  42. 30
      threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt
  43. 9
      threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt
  44. BIN
      tools/lwjglx-debug-1.0.0.jar

5
examples/graph-definition.xml

@ -1,5 +0,0 @@
<graph>
<box diameter="10" position="[33, 40, 3]">
</box>
</graph>

3
settings.gradle

@ -2,4 +2,5 @@ rootProject.name = 'threedom'
include 'threedom-kapt'
include 'threedom'
include 'threedom-gl'
include 'threedom-example'

67
threedom-example/build.gradle

@ -0,0 +1,67 @@
import org.gradle.internal.os.OperatingSystem
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
id 'java'
id 'application'
}
group 'me.eater.threedom'
version '1.0-SNAPSHOT'
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = "natives-windows"
break
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation platform("org.lwjgl:lwjgl-bom:3.2.3")
implementation project(":threedom")
implementation project(":threedom-gl")
implementation "org.lwjgl:lwjgl"
implementation "org.lwjgl:lwjgl-opengl"
implementation "org.lwjgl:lwjgl-glfw"
implementation 'org.joml:joml:1.9.24'
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives"
}
application {
mainClassName = 'me.eater.threedom.example.MainKt'
}
compileKotlin {
kotlinOptions.jvmTarget = "12"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "12"
}
java {
sourceCompatibility = JavaVersion.VERSION_12
targetCompatibility = JavaVersion.VERSION_12
}
run {
jvmArgs("-javaagent:$projectDir/../tools/lwjglx-debug-1.0.0.jar")
}

251
threedom-example/src/main/kotlin/me/eater/threedom/example/main.kt

@ -0,0 +1,251 @@
package me.eater.threedom.example
import me.eater.threedom.dom.Document
import me.eater.threedom.dom.IDocument
import me.eater.threedom.dom.PlainNode
import me.eater.threedom.dom.createNode
import me.eater.threedom.example.shader.Plain
import me.eater.threedom.example.vertex.Simple
import me.eater.threedom.gl.GL
import me.eater.threedom.gl.ShaderPreProcessor
import me.eater.threedom.gl.dom.PerspectiveCamera
import me.eater.threedom.gl.dom.TriMesh
import me.eater.threedom.gl.texture.Texture
import me.eater.threedom.gl.vertex.VertexArrayObject
import me.eater.threedom.gl.vertex.VertexBuffer
import me.eater.threedom.utils.joml.toFloat
import me.eater.threedom.utils.joml.translation
import me.eater.threedom.utils.joml.vec3
import org.joml.Vector2f
import org.joml.Vector3f
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.opengl.GL.createCapabilities
import org.lwjgl.opengl.GL11.*
import org.lwjgl.system.MemoryStack.stackPush
import org.lwjgl.system.MemoryUtil.NULL
import java.time.Instant
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
fun main() {
GLFWErrorCallback.createPrint(System.err).set()
if (!glfwInit()) {
println("Unable to initialize GLFW")
return
}
glfwDefaultWindowHints()
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
val window = glfwCreateWindow(300, 300, "Hello world", NULL, NULL)
if (window == NULL) {
println("Failed creating window")
return
}
val stack = stackPush()
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
// Get the window size passed to glfwCreateWindow
glfwGetWindowSize(window, pWidth, pHeight)
// Get the resolution of the primary monitor
val vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()) ?: return
// Center the window
glfwSetWindowPos(
window,
(vidmode.width() - pWidth.get(0)) / 2,
(vidmode.height() - pHeight.get(0)) / 2
)
// Make the OpenGL context current
glfwMakeContextCurrent(window)
var width = pWidth.get(0)
var height = pHeight.get(0)
// Enable v-sync
glfwSwapInterval(1)
glfwShowWindow(window)
createCapabilities()
val doc: IDocument = Document()
val triMesh = doc.createNode<TriMesh>()
val preProcessor = ShaderPreProcessor.createDefault()
val mesh = listOf(
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
)
val buffer = mesh
.chunked(6)
.map {
Simple().apply {
position = vec3(it[0], it[1], it[2])
normal = vec3(it[3], it[4], it[5])
}
}
.let {
VertexBuffer(it)
}
val VAO = VertexArrayObject {
buffer(Simple::position)
buffer(Simple::normal)
}
triMesh.mesh = VAO
println("Loading mesh")
println("Loading shader program")
val plainShader = Plain.load(preProcessor)
val lampShader = Plain.lamp(preProcessor)
val texture = Texture()
texture.load(Simple::class.java.getResourceAsStream("/textures/fujiwara.jpg").readAllBytes())
plainShader.apply {
objectColor = vec3(1.0, 0.5, 0.0)
lightColor = vec3(1.0, 1.0, 1.0)
}
triMesh.use(lampShader)
val empty = doc.createNode<PlainNode>()
doc.addNode(empty)
empty.addNode(triMesh)
val around = mutableListOf<TriMesh>()
val amount = 3
val deg = (PI * 2) / amount
for (i in 0 until amount) {
val trimeshDup = triMesh.clone()
trimeshDup.use(plainShader)
around.add(trimeshDup)
triMesh.addNode(trimeshDup)
trimeshDup.model {
translate(2 * sin(deg * i), 0.0, 2 * cos(deg * i))
}
}
val camera = doc.createNode<PerspectiveCamera>()
doc.addNode(camera)
glfwSetFramebufferSizeCallback(window) { _, newWidth, newHeight ->
glViewport(0, 0, newWidth, newHeight)
camera.width = newWidth.toDouble()
camera.height = newHeight.toDouble()
}
glfwSetKeyCallback(window) { _, key, _, action, _ ->
if ((key == GLFW_KEY_Q || key == GLFW_KEY_ESCAPE) && action == GLFW_RELEASE)
glfwSetWindowShouldClose(window, true)
var z = 0.0
var x = 0.0
val speed = 0.1
if (key == GLFW_KEY_UP && action != GLFW_RELEASE) {
z += speed
}
if (key == GLFW_KEY_DOWN && action != GLFW_RELEASE) {
z -= speed
}
if (key == GLFW_KEY_LEFT && action != GLFW_RELEASE) {
x += speed
}
if (key == GLFW_KEY_RIGHT && action != GLFW_RELEASE) {
x -= speed
}
if (x != 0.0 || z != 0.0) {
camera.model {
translateLocal(x, 0.0, z)
}
}
}
println("Starting render loop")
glClearColor(0.3f, 0.2f, 0.2f, 1.0f)
glEnable(GL_DEPTH_TEST)
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
empty.model {
setTranslation(0.0, 0.0, sin(Instant.now().toEpochMilli().toDouble() / 1000) - 3)
}
triMesh.model {
rotateY(0.005)
}
around.forEach {
it.model {
rotateY(-0.005)
}
}
plainShader.lightPos = triMesh.absolute.translation.toFloat()
plainShader.viewPos = camera.absolute.translation.toFloat()
doc.render(GL, camera)
glfwSwapBuffers(window)
glfwPollEvents()
}
}

60
threedom-example/src/main/kotlin/me/eater/threedom/example/shader/Plain.kt

@ -0,0 +1,60 @@
package me.eater.threedom.example.shader
import me.eater.threedom.gl.Shader
import me.eater.threedom.gl.ShaderPreProcessor
import me.eater.threedom.gl.ShaderProgram
class Plain private constructor(vertexShader: Shader, fragmentShader: Shader) :
ShaderProgram(vertexShader, fragmentShader) {
var objectColor by vec3()
var lightColor by vec3()
var lightPos by vec3()
var viewPos by vec3()
companion object {
fun load(preProcessor: ShaderPreProcessor): Plain {
val vertex = preProcessor.compile(
this::class.java.getResourceAsStream("/shaders/plain/vertex.glsl").readAllBytes()
.toString(Charsets.UTF_8),
Shader.ShaderType.Vertex
)
val fragment = preProcessor.compile(
this::class.java.getResourceAsStream("/shaders/plain/fragment.glsl").readAllBytes()
.toString(Charsets.UTF_8),
Shader.ShaderType.Fragment
)
return Plain(
vertex,
fragment
).also {
vertex.delete()
fragment.delete()
}
}
fun lamp(preProcessor: ShaderPreProcessor): Plain {
val vertex = preProcessor.compile(
this::class.java.getResourceAsStream("/shaders/plain/vertex.glsl").readAllBytes()
.toString(Charsets.UTF_8),
Shader.ShaderType.Vertex
)
val fragment = preProcessor.compile(
this::class.java.getResourceAsStream("/shaders/plain/fragment_lamp.glsl").readAllBytes()
.toString(Charsets.UTF_8),
Shader.ShaderType.Fragment
)
return Plain(
vertex,
fragment
).also {
vertex.delete()
fragment.delete()
}
}
}
}

8
threedom-example/src/main/kotlin/me/eater/threedom/example/vertex/Simple.kt

@ -0,0 +1,8 @@
package me.eater.threedom.example.vertex
import me.eater.threedom.gl.vertex.VertexData
class Simple : VertexData() {
var position by vec3()
var normal by vec3()
}

32
threedom-example/src/main/resources/shaders/plain/fragment.glsl

@ -0,0 +1,32 @@
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
in vec3 LightPos; // extra in variable, since we need the light position in view space we calculate this in the vertex shader
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(LightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(-FragPos); // the viewer is always at (0,0,0) in view-space, so viewDir is (0,0,0) - Position => -Position
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
}

7
threedom-example/src/main/resources/shaders/plain/fragment_lamp.glsl

@ -0,0 +1,7 @@
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0);
}

21
threedom-example/src/main/resources/shaders/plain/vertex.glsl

@ -0,0 +1,21 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
out vec3 LightPos;
uniform vec3 lightPos; // we now define the uniform in the vertex shader and pass the 'view space' lightpos to the fragment shader. lightPos is currently in world space.
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(view * model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(view * model))) * aNormal;
LightPos = vec3(view * vec4(lightPos, 1.0)); // Transform world-space light position to view-space light position
}

BIN
threedom-example/src/main/resources/textures/fujiwara.jpg

After

Width: 1080  |  Height: 1080  |  Size: 384 KiB

37
threedom-gl/build.gradle

@ -0,0 +1,37 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
id 'java'
}
group 'me.eater.threedom'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation platform("org.lwjgl:lwjgl-bom:3.2.3")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation project(":threedom")
implementation 'org.joml:joml:1.9.24'
implementation "org.lwjgl:lwjgl"
implementation "org.lwjgl:lwjgl-opengl"
implementation "org.lwjgl:lwjgl-stb"
}
compileKotlin {
kotlinOptions.jvmTarget = "12"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "12"
}
java {
sourceCompatibility = JavaVersion.VERSION_12
targetCompatibility = JavaVersion.VERSION_12
}

11
threedom-gl/src/main/kotlin/me/eater/threedom/gl/GL.kt

@ -0,0 +1,11 @@
package me.eater.threedom.gl
import me.eater.threedom.dom.render.IRenderTarget
import me.eater.threedom.gl.dom.ICamera
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryStack.stackPush
object GL : IRenderTarget<ICamera<*>> {
override val type = "GL"
val stack: MemoryStack = stackPush()
}

32
threedom-gl/src/main/kotlin/me/eater/threedom/gl/Shader.kt

@ -0,0 +1,32 @@
package me.eater.threedom.gl
import me.eater.threedom.gl.GL.stack
import me.eater.threedom.gl.exception.ShaderCompilationException
import org.lwjgl.opengl.GL20.*
import org.lwjgl.system.MemoryUtil
data class Shader(val shaderId: Int, val shaderType: ShaderType) {
enum class ShaderType(val glId: Int) {
Fragment(GL_FRAGMENT_SHADER),
Vertex(GL_VERTEX_SHADER);
}
fun delete() {
glDeleteShader(shaderId)
}
companion object {
fun create(source: String, shaderType: ShaderType): Shader {
val shaderId = glCreateShader(shaderType.glId)
glShaderSource(shaderId, source)
glCompileShader(shaderId)
val success = stack.mallocInt(1)
glGetShaderiv(shaderId, GL_COMPILE_STATUS, success)
if (success.get(0) == 0) {
throw ShaderCompilationException(glGetShaderInfoLog(shaderId))
}
return Shader(shaderId, shaderType)
}
}
}

46
threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderPreProcessor.kt

@ -0,0 +1,46 @@
package me.eater.threedom.gl
class ShaderPreProcessor {
private val regex = Regex("(?<module>[^:]+)::(?<call>[^:(]+)\\((?<args>.+)\\)")
private val calls: MutableMap<Pair<String, String>, (List<String>) -> String> = mutableMapOf()
fun process(input: String): String =
input.split("\n").joinToString("\n") {
if (it.startsWith("#[") && it.endsWith("]")) {
val macro = it.substring(2 until it.length - 1)
val match = regex.find(macro) ?: return@joinToString it
val module = match.groups["module"]!!.value
val call = match.groups["call"]!!.value
val args = match.groups["args"]!!.value.split(",").map { arg -> arg.trim() }.toList()
calls[module to call]?.invoke(args) ?: throw RuntimeException("Can't find macro $module::$call")
} else {
it
}
}
fun register(module: String, call: String, block: (List<String>) -> String) {
calls[module to call] = block
}
fun compile(source: String, type: Shader.ShaderType): Shader {
return Shader.create(process(source), type)
}
companion object {
fun createDefault() = ShaderPreProcessor().apply {
register("3dom", "import") { it ->
val knownUniforms = mapOf(
"model" to "mat4",
"view" to "mat4",
"projection" to "mat4",
"normalModel" to "mat3"
)
it.joinToString("\n") { uni ->
"uniform ${knownUniforms[uni]} $uni;"
}
}
}
}
}

220
threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderProgram.kt

@ -0,0 +1,220 @@
package me.eater.threedom.gl
import me.eater.threedom.gl.GL.stack
import me.eater.threedom.gl.dom.ICamera
import me.eater.threedom.gl.exception.ShaderProgramLinkingException
import me.eater.threedom.gl.texture.Texture
import me.eater.threedom.utils.joml.toFloat
import org.joml.*
import org.lwjgl.opengl.GL20.*
import org.lwjgl.system.MemoryUtil
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
open class ShaderProgram(private val programId: Int = glCreateProgram()) {
private val uniforms: MutableMap<String, Int> = mutableMapOf()
val queuedUniformValues: MutableMap<Int, () -> Unit> = mutableMapOf()
private val textures: MutableMap<Int, Int> = mutableMapOf()
private var textureIndex: Int = 0
var model by mat4()
var normalModel by mat3()
var view by mat4()
var projection by mat4()
private fun getUniformLocation(name: String) = uniforms.getOrPut(name) {
glGetUniformLocation(programId, name)
}
protected fun sampler2D(
index: Int = textureIndex++,
name: String? = null
): ReadWriteProperty<ShaderProgram, Texture?> {
var setUniform = false
return object : ReadWriteProperty<ShaderProgram, Texture?> {
override fun getValue(thisRef: ShaderProgram, property: KProperty<*>): Texture? {
return Texture.getTexture(textures[index] ?: return null)
}
override fun setValue(thisRef: ShaderProgram, property: KProperty<*>, value: Texture?) {
textures[index] = value?.id ?: 0
if (setUniform) {
return
}
val loc = getUniformLocation(name ?: property.name)
if (loc == -1) {
return
}
if (glGetInteger(GL_CURRENT_PROGRAM) == thisRef.programId) {
glUniform1i(loc, index)
} else {
queuedUniformValues[loc] = {
glUniform1i(loc, index)
}
}
setUniform = true
}
}
}
protected fun <T> uniform(
name: String? = null,
setter: (location: Int, value: T) -> Unit,
getter: (programId: Int, location: Int) -> T?
) = UniformProperty(name, setter, getter)
open class UniformProperty<T>(
val name: String? = null,
val setter: (location: Int, value: T) -> Unit,
val getter: (programId: Int, location: Int) -> T?
) {
operator fun getValue(thisRef: ShaderProgram, property: KProperty<*>): T? {
val loc = thisRef.getUniformLocation(name ?: property.name)
if (loc == -1) {
return null
}
return getter(thisRef.programId, loc)
}
operator fun setValue(thisRef: ShaderProgram, property: KProperty<*>, value: T?) {
val valueNonNull = value ?: return
val loc = thisRef.getUniformLocation(name ?: property.name)
if (loc == -1) {
return
}
if (glGetInteger(GL_CURRENT_PROGRAM) == thisRef.programId) {
setter(loc, valueNonNull)
} else {
thisRef.queuedUniformValues[loc] = {
setter(loc, valueNonNull)
}
}
}
}
protected fun float(name: String? = null): UniformProperty<Float> =
uniform(name, ::glUniform1f, ::glGetUniformf)
protected fun int(name: String? = null): UniformProperty<Int> =
uniform(name, ::glUniform1i, ::glGetUniformi)
protected fun vec2(name: String? = null): UniformProperty<Vector2fc> =
uniform(name, { loc, value ->
val floatArray = MemoryUtil.memAllocFloat(2)
value.get(floatArray)
glUniform2fv(loc, floatArray)
MemoryUtil.memFree(floatArray)
}, { programId, location ->
val buffer = FloatArray(2)
glGetUniformfv(programId, location, buffer)
Vector2f().set(buffer)
})
protected fun vec3(name: String? = null): UniformProperty<Vector3fc> =
uniform(name, { loc, value ->
val floatArray = MemoryUtil.memAllocFloat(3)
value.get(floatArray)
glUniform3fv(loc, floatArray)
MemoryUtil.memFree(floatArray)
}, { programId, location ->
val buffer = FloatArray(3)
glGetUniformfv(programId, location, buffer)
Vector3f().set(buffer)
})
protected fun vec4(name: String? = null): UniformProperty<Vector4fc> =
uniform(name, { loc, value ->
val floatArray = MemoryUtil.memAllocFloat(4)
value.get(floatArray)
glUniform4fv(loc, floatArray)
MemoryUtil.memFree(floatArray)
}, { programId, location ->
val buffer = FloatArray(4)
glGetUniformfv(programId, location, buffer)
Vector4f().set(buffer)
})
protected fun mat3(name: String? = null): UniformProperty<Matrix3fc> =
uniform(name, { loc, value ->
val floatArray = FloatArray(3 * 3)
value.get(floatArray)
glUniformMatrix3fv(loc, false, floatArray)
}, { programId, location ->
val buffer = FloatArray(3 * 3)
glGetUniformfv(programId, location, buffer)
Matrix3f().set(buffer)
})
protected fun bool(name: String? = null): UniformProperty<Boolean> =
uniform(name, { loc, value ->
glUniform1i(loc, if (value) 1 else 0)
}, { programId, location ->
glGetUniformi(programId, location) == 1
})
protected fun mat4(name: String? = null): UniformProperty<Matrix4fc> =
uniform(name, { loc, value ->
val floatArray = FloatArray(4 * 4)
value.get(floatArray)
glUniformMatrix4fv(loc, false, floatArray)
}, { programId, location ->
val buffer = FloatArray(4 * 4)
glGetUniformfv(programId, location, buffer)
Matrix4f().set(buffer)
})
fun enable() {
glUseProgram(programId)
for ((_, queued) in queuedUniformValues) {
queued()
}
for ((index, texture) in textures) {
glActiveTexture(GL_TEXTURE0 + index)
glBindTexture(GL_TEXTURE_2D, texture)
}
queuedUniformValues.clear()
}
fun delete() {
glDeleteProgram(programId)
}
fun disable() {
glUseProgram(0)
}
fun uses(name: String): Boolean {
return getUniformLocation(name) != -1
}
fun camera(renderer: ICamera<*>) {
this.projection = renderer.projection.toFloat()
this.view = renderer.view.toFloat()
}
constructor(vertexShader: Shader, fragmentShader: Shader) : this() {
glAttachShader(programId, vertexShader.shaderId)
glAttachShader(programId, fragmentShader.shaderId)
glLinkProgram(programId)
val success = stack.mallocInt(1)
glGetProgramiv(programId, GL_LINK_STATUS, success)
if (success.get(0) == 0) {
throw ShaderProgramLinkingException(glGetProgramInfoLog(programId))
}
}
}

10
threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/GLRenderNode.kt

@ -0,0 +1,10 @@
package me.eater.threedom.gl.dom
import me.eater.threedom.dom.render.IRenderNode
import me.eater.threedom.gl.GL
interface GLRenderNode<T : IRenderNode<T, GL, ICamera<*>>> :
IRenderNode<T, GL, ICamera<*>> {
override val target: GL
get() = GL
}

12
threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/ICamera.kt

@ -0,0 +1,12 @@
package me.eater.threedom.gl.dom
import me.eater.threedom.dom.INode
import org.joml.Matrix4dc
interface ICamera<T : INode<T>> : INode<T> {
var width: Double
var height: Double
val projection: Matrix4dc
val view: Matrix4dc
}

53
threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/PerspectiveCamera.kt

@ -0,0 +1,53 @@
package me.eater.threedom.gl.dom
import me.eater.threedom.dom.IDocument
import me.eater.threedom.dom.Node
import org.joml.Matrix4d
import org.joml.Matrix4dc
class PerspectiveCamera(document: IDocument?) : Node<PerspectiveCamera>(document), ICamera<PerspectiveCamera> {
override fun cloneSelf(): PerspectiveCamera =
PerspectiveCamera(document)
var fieldOfView: Double = 45.0
set(value) {
field = value
updateProjection()
}
var far: Double = 100.0
set(value) {
field = value
updateProjection()
}
var near: Double = 0.1
set(value) {
field = value
updateProjection()
}
override var width: Double = 800.0
set(value) {
field = value
updateProjection()
}
override var height: Double = 600.0
set(value) {
field = value
updateProjection()
}
private fun updateProjection() {
projection = makeProjection()
}
private fun makeProjection(): Matrix4dc = Matrix4d().perspective(fieldOfView, width / height, near, far)
override var projection: Matrix4dc = makeProjection()
private set
override val view: Matrix4dc
get() = absolute
}

49
threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/TriMesh.kt

@ -0,0 +1,49 @@
package me.eater.threedom.gl.dom
import me.eater.threedom.dom.IDocument
import me.eater.threedom.dom.Node
import me.eater.threedom.gl.ShaderProgram
import me.eater.threedom.gl.vertex.VertexArrayObject
import me.eater.threedom.utils.joml.toFloat
import org.joml.Matrix3d
import org.joml.Matrix4d
import org.lwjgl.opengl.GL30.*
open class TriMesh(document: IDocument?) : Node<TriMesh>(document), GLRenderNode<TriMesh> {
var mesh: VertexArrayObject? = null
var program: ShaderProgram? = null
private set
var programConfig: (ShaderProgram.(TriMesh) -> Unit)? = null
private set
fun <T : ShaderProgram> use(program: T, block: T.(TriMesh) -> Unit) {
this.program = program
@kotlin.Suppress("UNCHECKED_CAST")
this.programConfig = block as ShaderProgram.(TriMesh) -> Unit
}
fun <T : ShaderProgram> use(program: T) {
this.program = program
this.programConfig = null
}
override fun cloneSelf(): TriMesh {
return TriMesh(document).also {
it.mesh = mesh
it.program = program
it.programConfig = programConfig
}
}
override fun render(renderer: ICamera<*>) {
val mesh = mesh ?: return
val program = program ?: return
program.enable()
programConfig?.let { program.it(this) }
program.camera(renderer)
program.model = absolute.toFloat()
program.normalModel = model.invert(Matrix4d()).transpose().get3x3(Matrix3d()).toFloat()
glBindVertexArray(mesh.id)
glDrawArrays(GL_TRIANGLES, 0, mesh.size)
}
}

3
threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderCompilationException.kt

@ -0,0 +1,3 @@
package me.eater.threedom.gl.exception
data class ShaderCompilationException(val infoLog: String) : RuntimeException("Shader failed compiling: $infoLog")

4
threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderProgramLinkingException.kt

@ -0,0 +1,4 @@
package me.eater.threedom.gl.exception
data class ShaderProgramLinkingException(val infoLog: String) :
RuntimeException("Failed linking ShaderProgram: $infoLog")

8
threedom-gl/src/main/kotlin/me/eater/threedom/gl/geometry/Plane.kt

@ -0,0 +1,8 @@
package me.eater.threedom.gl.geometry
import me.eater.threedom.dom.IDocument
import me.eater.threedom.dom.INode
import me.eater.threedom.gl.dom.TriMesh
class Plane(document: IDocument?) : TriMesh(document) {
}

55
threedom-gl/src/main/kotlin/me/eater/threedom/gl/texture/Texture.kt

@ -0,0 +1,55 @@
package me.eater.threedom.gl.texture
import me.eater.threedom.gl.GL.stack
import org.lwjgl.opengl.GL30.*
import org.lwjgl.stb.STBImage.stbi_load_from_memory
import org.lwjgl.stb.STBImage.stbi_set_flip_vertically_on_load
import org.lwjgl.system.MemoryUtil
import java.lang.ref.WeakReference
import java.nio.ByteBuffer
class Texture {
var id: Int = run {
val pTexture = stack.mallocInt(1)
glGenTextures(pTexture)
pTexture.get(0).also {
textures[it] = WeakReference(this)
}
}
fun load(image: ByteArray, flipped: Boolean = true, mipmapLevel: Int = 0) {
val mem = MemoryUtil.memAlloc(image.size)
mem.put(image)
mem.rewind()
load(mem, flipped, mipmapLevel)
MemoryUtil.memFree(mem)
}
fun load(image: ByteBuffer, flipped: Boolean = true, mipmapLevel: Int = 0) {
stbi_set_flip_vertically_on_load(flipped)
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
val pChannels = stack.mallocInt(1)
val imagePixelData = stbi_load_from_memory(image, pWidth, pHeight, pChannels, 4)
glBindTexture(GL_TEXTURE_2D, id)
glTexImage2D(
GL_TEXTURE_2D,
mipmapLevel,
GL_RGBA8,
pWidth.get(0),
pHeight.get(0),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
imagePixelData
)
glGenerateMipmap(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, 0)
}
companion object {
private val textures = mutableMapOf<Int, WeakReference<Texture>>()
fun getTexture(id: Int): Texture? = textures[id]?.get()
}
}

78
threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexArrayObject.kt

@ -0,0 +1,78 @@
package me.eater.threedom.gl.vertex
import me.eater.threedom.gl.GL.stack
import org.lwjgl.opengl.GL30.*
import kotlin.math.min
import kotlin.reflect.KProperty1
class VertexArrayObject() {
var size: Int = 0
val id = run {
val pId = stack.mallocInt(1)
glGenVertexArrays(pId)
pId.get(0)
}
constructor(block: VertexArrayObject.() -> Unit) : this() {
block(this)
}
var pointerIndex: Int = 0
operator fun <T : VertexData> VertexBuffer<T>.invoke(prop: KProperty1<T, *>) =
bind(this, prop)
fun <T : VertexData> bind(vertexBuffer: VertexBuffer<T>, prop: KProperty1<T, *>) {
if (vertexBuffer.items.isEmpty()) {
return
}
size = if (pointerIndex == 0) {
vertexBuffer.items.size
} else {
min(size, vertexBuffer.items.size)
}
val first = vertexBuffer.items.first()
val item = first.getDataPoint(prop) ?: return
var size = item.slots
var length = item.length
var repeat = 1
var i = 4
while (size > 4) {
if ((size % i) == 0) {
repeat = size / i
size = i
length /= i
break
}
i--
}
val stride = if (vertexBuffer.interleaved) {
vertexBuffer.stride
} else {
item.length
}
var offset = first.getOffset(prop)!!
if (!vertexBuffer.interleaved) {
offset *= vertexBuffer.items.size
}
glBindVertexArray(id)
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer.id)
for (x in 0 until repeat) {
val pointer = pointerIndex++
glVertexAttribPointer(pointer, size, item.type, false, stride, offset + (x * length).toLong())
glEnableVertexAttribArray(pointer)
}
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
}
}

45
threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBuffer.kt

@ -0,0 +1,45 @@
package me.eater.threedom.gl.vertex
import me.eater.threedom.gl.GL.stack
import org.lwjgl.opengl.GL30.*
import org.lwjgl.system.MemoryUtil
import java.nio.ByteBuffer
open class VertexBuffer<T : VertexData>(
items: List<T> = listOf(),
val interleaved: Boolean = false,
val usage: Int = GL_STATIC_DRAW
) {
val id: Int = run {
val pId = stack.mallocInt(1)
glGenBuffers(pId)
pId.get(0)
}
@Suppress("CanBePrimaryConstructorProperty")
open val items: List<T> = items
var stride: Int
private set
init {
val size = items.size * (items.firstOrNull()?.length ?: 0).also { stride = it }
val byteBuffer = MemoryUtil.memAlloc(size)
var offset = 0
for (i in items.indices) {
val item = items[i]
if (interleaved) {
item.write(offset, byteBuffer)
offset += item.length
} else {
item.writeNonInterleaved(i, items.size, byteBuffer)
}
}
glBindBuffer(GL_ARRAY_BUFFER, id)
glBufferData(GL_ARRAY_BUFFER, byteBuffer, usage)
glBindBuffer(GL_ARRAY_BUFFER, 0)
MemoryUtil.memFree(byteBuffer)
}
}

3
threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBufferMutable.kt

@ -0,0 +1,3 @@
package me.eater.threedom.gl.vertex
class VertexBufferMutable<T: VertexData>(override val items: MutableList<T> = mutableListOf()) : VertexBuffer<T>()

56
threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexData.kt

@ -0,0 +1,56 @@
package me.eater.threedom.gl.vertex
import me.eater.threedom.gl.vertex.VertexDataPoint.*
import org.joml.*
import java.nio.ByteBuffer
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
abstract class VertexData() {
private val items = linkedMapOf<String, Pair<Int, VertexDataPoint<*>>>()
private var offset = 0
val length
get() = items.map { (_, v) -> v.component2().length }.sum()
class VertexDataRegister<T>(private val default: T, val provider: (default: T) -> VertexDataPoint<T>) {
operator fun provideDelegate(thisRef: VertexData, prop: KProperty<*>): VertexDataPoint<T> {
val delegate = provider(default)
thisRef.items[prop.name] = thisRef.offset to delegate
thisRef.offset += delegate.length
return delegate
}
}
fun int(value: Int = 0): VertexDataRegister<Int> = VertexDataRegister(value, ::SingleInt)
fun double(value: Double = 0.0): VertexDataRegister<Double> = VertexDataRegister(value, ::SingleDouble)
fun float(value: Float = 0f): VertexDataRegister<Float> = VertexDataRegister(value, ::SingleFloat)
fun bool(value: Boolean = false): VertexDataRegister<Boolean> = VertexDataRegister(value, ::SingleBoolean)
fun vec2(value: Vector2fc = Vector2f()): VertexDataRegister<Vector2fc> = VertexDataRegister(value, ::Vec2)
fun vec3(value: Vector3fc = Vector3f()): VertexDataRegister<Vector3fc> = VertexDataRegister(value, ::Vec3)
fun vec4(value: Vector4fc = Vector4f()): VertexDataRegister<Vector4fc> = VertexDataRegister(value, ::Vec4)
fun mat3(value: Matrix3fc = Matrix3f()): VertexDataRegister<Matrix3fc> = VertexDataRegister(value, ::Mat3)
fun mat4(value: Matrix4fc = Matrix4f()): VertexDataRegister<Matrix4fc> = VertexDataRegister(value, ::Mat4)
fun getDataPoint(prop: KProperty1<out VertexData, *>): VertexDataPoint<*>? = items[prop.name]?.component2()
fun getOffset(prop: KProperty1<out VertexData, *>): Int? = items[prop.name]?.component1()
fun writeNonInterleaved(position: Int, size: Int, byteBuffer: ByteBuffer) {
var offset = 0;
for ((_, pair) in items) {
val (_, item) = pair
item.write(offset + (position * item.length), byteBuffer)
offset += item.length * size
}
}
fun write(position: Int, byteBuffer: ByteBuffer) {
var pos = position
for ((_, pair) in items) {
val (_, item) = pair
item.write(pos, byteBuffer)
pos += item.length
}
}
}

73
threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexDataPoint.kt

@ -0,0 +1,73 @@
package me.eater.threedom.gl.vertex
import org.joml.*
import org.lwjgl.opengl.GL30.*
import java.nio.ByteBuffer
import kotlin.reflect.KProperty
abstract class VertexDataPoint<T>(val type: Int, var value: T, val length: Int, val slots: Int = 1) {
abstract fun write(position: Int, buffer: ByteBuffer)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
class SingleInt(value: Int) : VertexDataPoint<Int>(GL_INT, value, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putInt(position, value)
}
}
class SingleFloat(value: Float) : VertexDataPoint<Float>(GL_FLOAT, value, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putFloat(position, value)
}
}
class SingleDouble(value: Double) : VertexDataPoint<Double>(GL_DOUBLE, value, 8) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putDouble(position, value)
}
}
class SingleBoolean(value: Boolean) : VertexDataPoint<Boolean>(GL_BOOL, value, 1) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.put(position, if (value) 1 else 2)
}
}
class Vec2(value: Vector2fc) : VertexDataPoint<Vector2fc>(GL_FLOAT, value, 8, 2) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Vec3(value: Vector3fc) : VertexDataPoint<Vector3fc>(GL_FLOAT, value, 12, 3) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Vec4(value: Vector4fc) : VertexDataPoint<Vector4fc>(GL_FLOAT, value, 16, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Mat3(value: Matrix3fc) : VertexDataPoint<Matrix3fc>(GL_FLOAT, value, 3 * 3 * 4, 9) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Mat4(value: Matrix4fc) : VertexDataPoint<Matrix4fc>(GL_FLOAT, value, 4 * 4 * 4, 16) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
}

10
threedom-kapt/build.gradle

@ -1,5 +1,6 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
id 'java'
}
group 'me.eater.threedom'
@ -11,8 +12,13 @@ dependencies {
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "12"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "12"
}
java {
sourceCompatibility = JavaVersion.VERSION_12
targetCompatibility = JavaVersion.VERSION_12
}

10
threedom/build.gradle

@ -1,6 +1,7 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
id "org.jetbrains.kotlin.kapt" version "1.3.72"
id 'java'
}
group 'me.eater.threedom'
@ -30,8 +31,13 @@ dependencies {
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "12"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "12"
}
java {
sourceCompatibility = JavaVersion.VERSION_12
targetCompatibility = JavaVersion.VERSION_12
}

102
threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt

@ -1,9 +1,8 @@
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.dom.event.NodeModelUpdate
import me.eater.threedom.dom.event.*
import me.eater.threedom.dom.render.IRenderNode
import me.eater.threedom.dom.render.IRenderTarget
import me.eater.threedom.event.Event
import me.eater.threedom.generated.EventNames
import me.eater.threedom.utils.KDTree
@ -12,6 +11,7 @@ import kotlin.reflect.KClass
class Document : IDocument {
val root: PlainNode = PlainNode(this)
private val eventTree = EventTree {
on<DOMTreeUpdate.Remove> { (event) ->
removeNodeFromSearch(event.child)
@ -22,7 +22,9 @@ class Document : IDocument {
}
on<DOMTreeUpdate.Move> { (event) ->
updateKDTreeForNode(event.child)
if (event.child.shouldIndexLocation) {
updateKDTreeForNode(event.child)
}
}
on<NodeIDUpdate> { (event) ->
@ -36,19 +38,19 @@ class Document : IDocument {
}
on<NodeClassListUpdate.Added> { (event) ->
for (className in event.classNames) {
byClass.getOrPut(className, ::mutableSetOf).add(event.node.nodeId)
for (tagName in event.tagNames) {
byTag.getOrPut(tagName, ::mutableSetOf).add(event.node.nodeId)
}
}
on<NodeClassListUpdate.Removed> { (event) ->
for (className in event.classNames) {
val set = byClass[className] ?: continue
for (tagName in event.tagNames) {
val set = byTag[tagName] ?: continue
set.remove(event.node.nodeId)
if (set.size == 0) {
byClass.remove(className)
byTag.remove(tagName)
}
}
}
@ -56,20 +58,34 @@ class Document : IDocument {
on<NodeModelUpdate> { (event) ->
updateKDTreeForNode(event.node)
}
on<NodeLocationIndexStateChange> { (event) ->
if (event.shouldIndex) {
kdTree.add(event.node)
} else {
kdTree.remove(event.node)
}
}
}
private val kdTree = KDTree(this)
private val allNodes: MutableMap<Long, INode<*>> = mutableMapOf()
private val byId: MutableMap<String, Long> = mutableMapOf()
private val byClass: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byTag: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byType: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byRenderTarget: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private fun updateKDTreeForNode(node: INode<*>) {
node.updateAbsolute()
kdTree.update(node)
if (node.shouldIndexLocation) {
kdTree.update(node)
}
for (child in node.recursiveIterator()) {
child.updateAbsolute()
kdTree.update(child)
if (node.shouldIndexLocation) {
kdTree.update(child)
}
}
}
@ -107,8 +123,21 @@ class Document : IDocument {
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()
override fun getNodesByTag(tagName: String): Sequence<INode<*>> =
byTag[tagName]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence()
@Suppress("UNCHECKED_CAST")
override fun <T : INode<T>> getNodesByClass(className: String): Sequence<T> =
byType[className]?.asSequence()?.mapNotNull { nodeId ->
allNodes[nodeId]?.let {
(it.className == className) as? T
}
} ?: emptySequence()
@Suppress("UNCHECKED_CAST")
override fun <T : IRenderTarget<C>, C> getNodesByRenderTarget(targetType: String): Sequence<IRenderNode<*, T, C>> =
byRenderTarget[targetType]?.asSequence()?.mapNotNull(allNodes::get) as? Sequence<IRenderNode<*, T, C>>
?: emptySequence()
private fun addNodeToSearch(node: INode<*>) {
@ -116,11 +145,19 @@ class Document : IDocument {
node.id?.let { byId.putIfAbsent(it, node.nodeId) }
for (className in node.classList) {
byClass.getOrPut(className, ::mutableSetOf).add(node.nodeId)
for (className in node.tagList) {
byTag.getOrPut(className, ::mutableSetOf).add(node.nodeId)
}
byType.getOrPut(node.javaClass.name, ::mutableSetOf).add(node.nodeId)
if (node is IRenderNode<*, *, *>) {
byRenderTarget.getOrPut(node.target.type, ::mutableSetOf).add(node.nodeId)
}
kdTree.add(node)
if (node.shouldIndexLocation) {
kdTree.add(node)
}
}
private fun removeNodeFromSearch(node: INode<*>) {
@ -128,15 +165,36 @@ class Document : IDocument {
node.id?.let { byId.remove(it, node.nodeId) }
for (className in node.classList) {
val set = byClass.get(className) ?: continue
for (className in node.tagList) {
val set = byTag.get(className) ?: continue
set.remove(node.nodeId)
if (set.size == 0) {
byClass.remove(className)
byTag.remove(className)
}
}
byType[node.javaClass.name]?.let {
it.remove(node.nodeId)
if (it.size == 0) {
byType.remove(node.javaClass.name)
}
}
kdTree.remove(node)
if (node is IRenderNode<*, *, *>) {
byRenderTarget[node.target.type]?.let {
it.remove(node.nodeId)
if (it.size == 0) {
byRenderTarget.remove(node.target.type)
}
}
}
if (node.shouldIndexLocation) {
kdTree.remove(node)