From 3b57c3ce26295e8c48ea4039d235cd165963dc21 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Tue, 2 Jan 2024 18:13:14 +0100 Subject: [PATCH] [orx-kdtree] Make orx-kdtree a multiplatform module --- gradle/libs.versions.toml | 1 + orx-kdtree/README.md | 6 +- orx-kdtree/build.gradle.kts | 28 ++- .../src/{main => commonMain}/kotlin/KDTree.kt | 200 +++--------------- orx-kdtree/src/commonMain/kotlin/Mappers.kt | 61 ++++++ .../kotlin/QuickSelect.kt | 72 +++---- orx-kdtree/src/jsMain/kotlin/KDTree.kt | 71 +++++++ .../kotlin/DemoKNearestNeighbour01.kt | 0 .../kotlin/DemoNearestNeighbour01.kt | 0 .../kotlin/DemoRangeQuery01.kt | 0 orx-kdtree/src/jvmMain/kotlin/KDTree.kt | 80 +++++++ 11 files changed, 305 insertions(+), 214 deletions(-) rename orx-kdtree/src/{main => commonMain}/kotlin/KDTree.kt (63%) create mode 100644 orx-kdtree/src/commonMain/kotlin/Mappers.kt rename orx-kdtree/src/{main => commonMain}/kotlin/QuickSelect.kt (96%) create mode 100644 orx-kdtree/src/jsMain/kotlin/KDTree.kt rename orx-kdtree/src/{demo => jvmDemo}/kotlin/DemoKNearestNeighbour01.kt (100%) rename orx-kdtree/src/{demo => jvmDemo}/kotlin/DemoNearestNeighbour01.kt (100%) rename orx-kdtree/src/{demo => jvmDemo}/kotlin/DemoRangeQuery01.kt (100%) create mode 100644 orx-kdtree/src/jvmMain/kotlin/KDTree.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27460db7..f65512ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ kotest-framework-engine = { group = "io.kotest", name = "kotest-framework-engine openrndr-application = { group = "org.openrndr", name = "openrndr-application", version.ref = "openrndr" } openrndr-extensions = { group = "org.openrndr", name = "openrndr-extensions", version.ref = "openrndr" } openrndr-math = { group = "org.openrndr", name = "openrndr-math", version.ref = "openrndr" } +openrndr-utils = { group = "org.openrndr", name = "openrndr-utils", version.ref = "openrndr" } openrndr-shape = { group = "org.openrndr", name = "openrndr-shape", version.ref = "openrndr" } openrndr-draw = { group = "org.openrndr", name = "openrndr-draw", version.ref = "openrndr" } openrndr-event = { group = "org.openrndr", name = "openrndr-event", version.ref = "openrndr" } diff --git a/orx-kdtree/README.md b/orx-kdtree/README.md index 647fbf3b..9a8281d3 100644 --- a/orx-kdtree/README.md +++ b/orx-kdtree/README.md @@ -5,16 +5,16 @@ Fast search of points closest to the queried point in a data set. 2D, 3D and 4D. ## Demos ### DemoKNearestNeighbour01 -[source code](src/demo/kotlin/DemoKNearestNeighbour01.kt) +[source code](src/jvmDemo/kotlin/DemoKNearestNeighbour01.kt) ![DemoKNearestNeighbour01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-kdtree/images/DemoKNearestNeighbour01Kt.png) ### DemoNearestNeighbour01 -[source code](src/demo/kotlin/DemoNearestNeighbour01.kt) +[source code](src/jvmDemo/kotlin/DemoNearestNeighbour01.kt) ![DemoNearestNeighbour01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-kdtree/images/DemoNearestNeighbour01Kt.png) ### DemoRangeQuery01 -[source code](src/demo/kotlin/DemoRangeQuery01.kt) +[source code](src/jvmDemo/kotlin/DemoRangeQuery01.kt) ![DemoRangeQuery01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-kdtree/images/DemoRangeQuery01Kt.png) diff --git a/orx-kdtree/build.gradle.kts b/orx-kdtree/build.gradle.kts index a6c4d79d..3ffa5d3d 100644 --- a/orx-kdtree/build.gradle.kts +++ b/orx-kdtree/build.gradle.kts @@ -1,15 +1,23 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { - org.openrndr.extra.convention.`kotlin-jvm` + org.openrndr.extra.convention.`kotlin-multiplatform` } -tasks.withType { - kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.openrndr.application) + api(libs.openrndr.math) + implementation(libs.kotlin.coroutines) + api(libs.openrndr.utils) + } + } + } } -dependencies { - implementation(libs.openrndr.application) - implementation(libs.openrndr.math) - implementation(libs.kotlin.coroutines) -} \ No newline at end of file + + +//tasks.withType { +// kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") +//} diff --git a/orx-kdtree/src/main/kotlin/KDTree.kt b/orx-kdtree/src/commonMain/kotlin/KDTree.kt similarity index 63% rename from orx-kdtree/src/main/kotlin/KDTree.kt rename to orx-kdtree/src/commonMain/kotlin/KDTree.kt index 9c99bc3c..a8304937 100644 --- a/orx-kdtree/src/main/kotlin/KDTree.kt +++ b/orx-kdtree/src/commonMain/kotlin/KDTree.kt @@ -1,45 +1,10 @@ package org.openrndr.extra.kdtree -import kotlinx.coroutines.* -import org.openrndr.math.* -import java.util.* -import kotlin.IllegalStateException + +import org.openrndr.collections.PriorityQueue import kotlin.math.abs -/** built-in mapper for [Vector2] */ -fun vector2Mapper(v: Vector2, dimension: Int): Double { - return when (dimension) { - 0 -> v.x - else -> v.y - } -} -fun intVector2Mapper(v: IntVector2, dimension: Int): Double { - return when (dimension) { - 0 -> v.x.toDouble() - else -> v.y.toDouble() - } -} - - -/** built-in mapper for [Vector3] */ -fun vector3Mapper(v: Vector3, dimension: Int): Double { - return when (dimension) { - 0 -> v.x - 1 -> v.y - else -> v.z - } -} - -/** built-in mapper for [Vector4] */ -fun vector4Mapper(v: Vector4, dimension: Int): Double { - return when (dimension) { - 0 -> v.x - 1 -> v.y - 2 -> v.z - else -> v.w - } -} class KDTreeNode(val dimensions: Int, val mapper: (T, Int) -> Double) { var parent: KDTreeNode? = null @@ -76,8 +41,7 @@ class KDTreeNode(val dimensions: Int, val mapper: (T, Int) -> Double) { "median=" + median + ", item=" + item + ", dimension=" + dimension + - ", children=" + Arrays.toString(children) + - + ", children=" + children.joinToString(", ") { it.toString() } + "} " + super.toString() } } @@ -95,82 +59,8 @@ private fun insertItem(root: KDTreeNode, item: T): KDTreeNode { } } +expect fun buildKDTree(items: MutableList, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode -@OptIn(DelicateCoroutinesApi::class) -fun buildKDTree(items: MutableList, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { - val root = KDTreeNode(dimensions, mapper) - - fun buildTreeTask( - scope: CoroutineScope, - node: KDTreeNode, - items: MutableList, - dimensions: Int, - levels: Int, - mapper: (T, Int) -> Double - ): KDTreeNode { - - if (items.size > 0) { - val dimension = levels % dimensions - val values = ArrayList() - for (item in items) { - values.add(item) - } - - node.dimension = dimension - val median = selectNth(items, items.size / 2) { mapper(it, dimension) } - - val leftItems = mutableListOf() - val rightItems = mutableListOf() - - node.median = mapper(median, dimension) - node.item = median - for (item in items) { - if (item === median) { - continue - } - if (mapper(item, dimension) < node.median) { - leftItems.add(item) - } else { - rightItems.add(item) - } - } - - // validate split - if (leftItems.size + rightItems.size + 1 != items.size) { - throw IllegalStateException("left: ${leftItems.size}, right: ${rightItems.size}, items: ${items.size}") - } - - if (leftItems.size > 0) { - node.children[0] = KDTreeNode(dimensions, mapper) - node.children[0]?.let { - it.parent = node - - scope.launch { - buildTreeTask(scope, it, leftItems, dimensions, levels + 1, mapper) - } - } - } - if (rightItems.size > 0) { - node.children[1] = KDTreeNode(dimensions, mapper) - node.children[1]?.let { - it.parent = node - scope.launch { - buildTreeTask(scope, it, rightItems, dimensions, levels + 1, mapper) - } - } - } - } - return node - } - - val job = GlobalScope.launch { - buildTreeTask(this, root, items, dimensions, 0, mapper) - } - runBlocking { - job.join() - } - return root -} private fun sqrDistance(left: T, right: T, dimensions: Int, mapper: (T, Int) -> Double): Double { @@ -184,20 +74,18 @@ private fun sqrDistance(left: T, right: T, dimensions: Int, mapper: (T, Int) } fun findAllNodes(root: KDTreeNode): List> { - val stack = Stack>() - val all = ArrayList>() - stack.push(root) + val stack = mutableListOf>() + val all = mutableListOf>() + stack.add(root) while (!stack.isEmpty()) { - val node = stack.pop() -// if (node.item != null /*&& !visited.contains(node.children[1])*/) { + val node = stack.removeLast() all.add(node) -// } - if (node.children[0] != null /*&&!visited.contains(node.children[0])*/) { - stack.push(node.children[0]) + if (node.children[0] != null) { + stack.add(node.children[0] !!) } if (node.children[1] != null) { - stack.push(node.children[1]) + stack.add(node.children[1] !!) } } return all @@ -229,16 +117,16 @@ fun findKNearest( val distance = sqrDistance(query, node.item ?: error("item is null"), node.dimensions, node.mapper) if (includeQuery || node.item !== query) { - if (queue.size < k || distance < queue.peek().second) { + if (queue.size() < k || distance < queue.peek().second) { queue.add(Pair(node, distance)) - if (queue.size > k) { + if (queue.size() > k) { queue.poll() } } } val d = abs(node.median - dimensionValue) - if (queue.size < k || d * d < queue.peek().second) { + if (queue.size() < k || d * d < queue.peek().second) { nearest(node.children[1 - route]) } } @@ -253,7 +141,7 @@ fun findKNearest( } private fun findNearest(root: KDTreeNode, query: T, includeQuery: Boolean = false): T? { - var nearest = java.lang.Double.POSITIVE_INFINITY + var nearest = Double.POSITIVE_INFINITY var nearestArg: KDTreeNode? = null fun nearest(node: KDTreeNode?) { @@ -309,10 +197,10 @@ private fun findAllInRadius( } val route: Int = if ((dimensionValue < node.median || node.children[1]==null) && node.children[0] != null ) { - queue.add(node.children[0]) + queue.add(node.children[0]!!) 0 } else if (node.children[1] != null) { - queue.add(node.children[1]) + queue.add(node.children[1]!!) 1 } else { -1 @@ -333,18 +221,18 @@ private fun findAllInRadius( } private fun insert(root: KDTreeNode, item: T, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { - val stack = Stack>() - stack.push(root) + val stack = mutableListOf>() + stack.add(root) dive@ while (true) { - val node = stack.peek() + val node = stack.last() val value = mapper(item, node.dimension) if (value < node.median) { if (node.children[0] != null) { - stack.push(node.children[0]) + stack.add(node.children[0]!!) } else { // sit here node.children[0] = KDTreeNode(dimensions, mapper) @@ -356,7 +244,7 @@ private fun insert(root: KDTreeNode, item: T, dimensions: Int, mapper: (T } } else { if (node.children[1] != null) { - stack.push(node.children[1]) + stack.add(node.children[1]!!) } else { // sit here node.children[1] = KDTreeNode(dimensions, mapper) @@ -387,27 +275,27 @@ private fun remove(toRemove: KDTreeNode, mapper: (T, Int) -> Double): KDT toRemove.item = null } } else { - val stack = Stack>() + val stack = mutableListOf>() var branch = 0 if (toRemove.children[0] != null) { - stack.push(toRemove.children[0]) + stack.add(toRemove.children[0]!!) branch = 0 } else { - stack.push(toRemove.children[1]) + stack.add(toRemove.children[1]!!) branch = 1 } - var minValue: Double = java.lang.Double.POSITIVE_INFINITY - var maxValue: Double = java.lang.Double.NEGATIVE_INFINITY + var minValue: Double = Double.POSITIVE_INFINITY + var maxValue: Double = Double.NEGATIVE_INFINITY var minArg: KDTreeNode? = null var maxArg: KDTreeNode? = null while (!stack.isEmpty()) { - val node = stack.pop() ?: throw RuntimeException("null on stack") + val node = stack.removeLast() - val value = mapper(node.item ?: throw IllegalStateException("item is null"), toRemove.dimension) + val value = mapper(node.item ?: error("item is null"), toRemove.dimension) if (value < minValue) { minValue = value @@ -421,27 +309,27 @@ private fun remove(toRemove: KDTreeNode, mapper: (T, Int) -> Double): KDT if (node.dimension != toRemove.dimension) { if (node.children[0] != null) { - stack.push(node.children[0]) + stack.add(node.children[0]!!) } if (node.children[1] != null) { - stack.push(node.children[1]) + stack.add(node.children[1]!!) } } else { if (branch == 1) { if (node.children[0] != null) { - stack.push(node.children[0]) + stack.add(node.children[0]!!) } else { if (node.children[1] != null) { - stack.push(node.children[1]) + stack.add(node.children[1]!!) } } } if (branch == 0) { if (node.children[1] != null) { - stack.push(node.children[1]) + stack.add(node.children[1]!!) } else { if (node.children[0] != null) { - stack.push(node.children[0]) + stack.add(node.children[0]!!) } } } @@ -462,21 +350,3 @@ private fun remove(toRemove: KDTreeNode, mapper: (T, Int) -> Double): KDT } return null } - -@JvmName("kdTreeVector2") -fun Iterable.kdTree(): KDTreeNode { - val items = this.toMutableList() - return buildKDTree(items, 2, ::vector2Mapper) -} - -@JvmName("kdTreeVector3") -fun Iterable.kdTree(): KDTreeNode { - val items = this.toMutableList() - return buildKDTree(items, 3, ::vector3Mapper) -} - -@JvmName("kdTreeVector4") -fun Iterable.kdTree(): KDTreeNode { - val items = this.toMutableList() - return buildKDTree(items, 4, ::vector4Mapper) -} \ No newline at end of file diff --git a/orx-kdtree/src/commonMain/kotlin/Mappers.kt b/orx-kdtree/src/commonMain/kotlin/Mappers.kt new file mode 100644 index 00000000..5371e5a2 --- /dev/null +++ b/orx-kdtree/src/commonMain/kotlin/Mappers.kt @@ -0,0 +1,61 @@ +package org.openrndr.extra.kdtree +import org.openrndr.math.Vector2 +import org.openrndr.math.IntVector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.Vector4 +import kotlin.jvm.JvmName + +/** built-in mapper for [Vector2] */ +fun vector2Mapper(v: Vector2, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + else -> v.y + } +} + +fun intVector2Mapper(v: IntVector2, dimension: Int): Double { + return when (dimension) { + 0 -> v.x.toDouble() + else -> v.y.toDouble() + } +} + + +/** built-in mapper for [Vector3] */ +fun vector3Mapper(v: Vector3, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + 1 -> v.y + else -> v.z + } +} + +/** built-in mapper for [Vector4] */ +fun vector4Mapper(v: Vector4, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + 1 -> v.y + 2 -> v.z + else -> v.w + } +} + + + +@JvmName("kdTreeVector2") +fun Iterable.kdTree(): KDTreeNode { + val items = this.toMutableList() + return buildKDTree(items, 2, ::vector2Mapper) +} + +@JvmName("kdTreeVector3") +fun Iterable.kdTree(): KDTreeNode { + val items = this.toMutableList() + return buildKDTree(items, 3, ::vector3Mapper) +} + +@JvmName("kdTreeVector4") +fun Iterable.kdTree(): KDTreeNode { + val items = this.toMutableList() + return buildKDTree(items, 4, ::vector4Mapper) +} \ No newline at end of file diff --git a/orx-kdtree/src/main/kotlin/QuickSelect.kt b/orx-kdtree/src/commonMain/kotlin/QuickSelect.kt similarity index 96% rename from orx-kdtree/src/main/kotlin/QuickSelect.kt rename to orx-kdtree/src/commonMain/kotlin/QuickSelect.kt index da29d207..e5ed7750 100644 --- a/orx-kdtree/src/main/kotlin/QuickSelect.kt +++ b/orx-kdtree/src/commonMain/kotlin/QuickSelect.kt @@ -1,37 +1,37 @@ -package org.openrndr.extra.kdtree - -fun selectNth(items: MutableList, n: Int, mapper: (T)->Double): T { - var from = 0 - var to = items.size - 1 - - // if from == to we reached the kth element - while (from < to) { - var r = from - var w = to - val mid = mapper(items[(r + w) / 2]) - - // stop when the reader and writer meet - while (r < w) { - if (mapper(items[r]) >= mid) { // put the large values at the end - val tmp = items[w] - items[w] = items[r] - items[r] = tmp - w-- - } else { // the value is smaller than the pivot, skip - r++ - } - } - - // if we stepped up (r++) we need to step one down - if (mapper(items[r]) > mid) - r-- - - // the r pointer is on the end of the first k elements - if (n <= r) { - to = r - } else { - from = r + 1 - } - } - return items[n] +package org.openrndr.extra.kdtree + +fun selectNth(items: MutableList, n: Int, mapper: (T)->Double): T { + var from = 0 + var to = items.size - 1 + + // if from == to we reached the kth element + while (from < to) { + var r = from + var w = to + val mid = mapper(items[(r + w) / 2]) + + // stop when the reader and writer meet + while (r < w) { + if (mapper(items[r]) >= mid) { // put the large values at the end + val tmp = items[w] + items[w] = items[r] + items[r] = tmp + w-- + } else { // the value is smaller than the pivot, skip + r++ + } + } + + // if we stepped up (r++) we need to step one down + if (mapper(items[r]) > mid) + r-- + + // the r pointer is on the end of the first k elements + if (n <= r) { + to = r + } else { + from = r + 1 + } + } + return items[n] } \ No newline at end of file diff --git a/orx-kdtree/src/jsMain/kotlin/KDTree.kt b/orx-kdtree/src/jsMain/kotlin/KDTree.kt new file mode 100644 index 00000000..899e152b --- /dev/null +++ b/orx-kdtree/src/jsMain/kotlin/KDTree.kt @@ -0,0 +1,71 @@ +package org.openrndr.extra.kdtree + + +actual fun buildKDTree(items: MutableList, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { + val root = KDTreeNode(dimensions, mapper) + + fun buildTreeTask( + node: KDTreeNode, + items: MutableList, + dimensions: Int, + levels: Int, + mapper: (T, Int) -> Double + ): KDTreeNode { + + if (items.size > 0) { + val dimension = levels % dimensions + val values = ArrayList() + for (item in items) { + values.add(item) + } + + node.dimension = dimension + val median = selectNth(items, items.size / 2) { mapper(it, dimension) } + + val leftItems = mutableListOf() + val rightItems = mutableListOf() + + node.median = mapper(median, dimension) + node.item = median + for (item in items) { + if (item === median) { + continue + } + if (mapper(item, dimension) < node.median) { + leftItems.add(item) + } else { + rightItems.add(item) + } + } + + // validate split + if (leftItems.size + rightItems.size + 1 != items.size) { + throw IllegalStateException("left: ${leftItems.size}, right: ${rightItems.size}, items: ${items.size}") + } + + if (leftItems.size > 0) { + node.children[0] = KDTreeNode(dimensions, mapper) + node.children[0]?.let { + it.parent = node + + + buildTreeTask(it, leftItems, dimensions, levels + 1, mapper) + + } + } + if (rightItems.size > 0) { + node.children[1] = KDTreeNode(dimensions, mapper) + node.children[1]?.let { + it.parent = node + buildTreeTask(it, rightItems, dimensions, levels + 1, mapper) + + } + } + } + return node + } + + + buildTreeTask(root, items, dimensions, 0, mapper) + return root +} diff --git a/orx-kdtree/src/demo/kotlin/DemoKNearestNeighbour01.kt b/orx-kdtree/src/jvmDemo/kotlin/DemoKNearestNeighbour01.kt similarity index 100% rename from orx-kdtree/src/demo/kotlin/DemoKNearestNeighbour01.kt rename to orx-kdtree/src/jvmDemo/kotlin/DemoKNearestNeighbour01.kt diff --git a/orx-kdtree/src/demo/kotlin/DemoNearestNeighbour01.kt b/orx-kdtree/src/jvmDemo/kotlin/DemoNearestNeighbour01.kt similarity index 100% rename from orx-kdtree/src/demo/kotlin/DemoNearestNeighbour01.kt rename to orx-kdtree/src/jvmDemo/kotlin/DemoNearestNeighbour01.kt diff --git a/orx-kdtree/src/demo/kotlin/DemoRangeQuery01.kt b/orx-kdtree/src/jvmDemo/kotlin/DemoRangeQuery01.kt similarity index 100% rename from orx-kdtree/src/demo/kotlin/DemoRangeQuery01.kt rename to orx-kdtree/src/jvmDemo/kotlin/DemoRangeQuery01.kt diff --git a/orx-kdtree/src/jvmMain/kotlin/KDTree.kt b/orx-kdtree/src/jvmMain/kotlin/KDTree.kt new file mode 100644 index 00000000..ef8efc00 --- /dev/null +++ b/orx-kdtree/src/jvmMain/kotlin/KDTree.kt @@ -0,0 +1,80 @@ +@file:JvmName("KDTreeJvmKt") +package org.openrndr.extra.kdtree + +import kotlinx.coroutines.* + +@OptIn(DelicateCoroutinesApi::class) +actual fun buildKDTree(items: MutableList, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { + val root = KDTreeNode(dimensions, mapper) + + fun buildTreeTask( + scope: CoroutineScope, + node: KDTreeNode, + items: MutableList, + dimensions: Int, + levels: Int, + mapper: (T, Int) -> Double + ): KDTreeNode { + + if (items.size > 0) { + val dimension = levels % dimensions + val values = ArrayList() + for (item in items) { + values.add(item) + } + + node.dimension = dimension + val median = selectNth(items, items.size / 2) { mapper(it, dimension) } + + val leftItems = mutableListOf() + val rightItems = mutableListOf() + + node.median = mapper(median, dimension) + node.item = median + for (item in items) { + if (item === median) { + continue + } + if (mapper(item, dimension) < node.median) { + leftItems.add(item) + } else { + rightItems.add(item) + } + } + + // validate split + if (leftItems.size + rightItems.size + 1 != items.size) { + throw IllegalStateException("left: ${leftItems.size}, right: ${rightItems.size}, items: ${items.size}") + } + + if (leftItems.size > 0) { + node.children[0] = KDTreeNode(dimensions, mapper) + node.children[0]?.let { + it.parent = node + + scope.launch { + buildTreeTask(scope, it, leftItems, dimensions, levels + 1, mapper) + } + } + } + if (rightItems.size > 0) { + node.children[1] = KDTreeNode(dimensions, mapper) + node.children[1]?.let { + it.parent = node + scope.launch { + buildTreeTask(scope, it, rightItems, dimensions, levels + 1, mapper) + } + } + } + } + return node + } + + val job = GlobalScope.launch { + buildTreeTask(this, root, items, dimensions, 0, mapper) + } + runBlocking { + job.join() + } + return root +}