diff --git a/orx-quadtree/README.md b/orx-quadtree/README.md new file mode 100644 index 00000000..8f428f4c --- /dev/null +++ b/orx-quadtree/README.md @@ -0,0 +1,22 @@ +# orx-quadtree + +An extension for creating a [Quadtree](https://en.wikipedia.org/wiki/Quadtree) for points. A quadtree is a spatial +partioning tree structure meant to provide fast spatial queries such as nearest points within a range. + +## Example + +```kotlin +val box = Rectangle.fromCenter(Vector2(400.0), 750.0) + +val quadTree = Quadtree(box) { it } + +for (point in points) { + quadTree.insert(point) +} + +val nearestQuery = quadTree.nearest(points[4], 20.0) +``` + +### Author + +Ricardo Matias / [@ricardomatias](https://github.com/ricardomatias) diff --git a/orx-quadtree/build.gradle b/orx-quadtree/build.gradle new file mode 100644 index 00000000..98a1f015 --- /dev/null +++ b/orx-quadtree/build.gradle @@ -0,0 +1,19 @@ +sourceSets { + demo { + java { + srcDirs = ["src/demo/kotlin"] + compileClasspath += main.getCompileClasspath() + runtimeClasspath += main.getRuntimeClasspath() + } + } +} + +dependencies { + demoImplementation("org.openrndr:openrndr-core:$openrndrVersion") + demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion") + demoImplementation(project(":orx-noise")) + demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") + demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion") + + demoImplementation(sourceSets.getByName("main").output) +} \ No newline at end of file diff --git a/orx-quadtree/src/demo/kotlin/DemoQuadTree01.kt b/orx-quadtree/src/demo/kotlin/DemoQuadTree01.kt new file mode 100644 index 00000000..bad546b7 --- /dev/null +++ b/orx-quadtree/src/demo/kotlin/DemoQuadTree01.kt @@ -0,0 +1,55 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.rectangleBatch +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.noise.Random +import org.openrndr.extra.noise.gaussian +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import quadtree.Quadtree + +fun main() { + application { + configure { + width = 800 + height = 800 + title = "QuadTree" + } + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val box = Rectangle.fromCenter(Vector2(400.0), 750.0) + + val points = (0 until 1_000).map { + Vector2.gaussian(box.center, Vector2(95.0), Random.rnd) + } + + val quadTree = Quadtree(box) { it } + + for (point in points) { + quadTree.insert(point) + } + + val batch = drawer.rectangleBatch { + this.fill = null + this.stroke = ColorRGBa.GRAY + this.strokeWeight = 0.5 + quadTree.batch(this) + } + + extend { + drawer.clear(ColorRGBa.BLACK) + + drawer.rectangles(batch) + + drawer.fill = ColorRGBa.PINK.opacify(0.7) + drawer.stroke = null + drawer.circles(points, 5.0) + } + } + } +} \ No newline at end of file diff --git a/orx-quadtree/src/demo/kotlin/DemoQuadTree02.kt b/orx-quadtree/src/demo/kotlin/DemoQuadTree02.kt new file mode 100644 index 00000000..f6d84705 --- /dev/null +++ b/orx-quadtree/src/demo/kotlin/DemoQuadTree02.kt @@ -0,0 +1,82 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.rectangleBatch +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.noise.Random +import org.openrndr.extra.noise.gaussian +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import quadtree.Quadtree + +fun main() { + application { + configure { + width = 800 + height = 800 + title = "QuadTree" + } + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val box = Rectangle.fromCenter(Vector2(400.0), 750.0) + + val points = (0 until 100).map { + Vector2.gaussian(box.center, Vector2(95.0), Random.rnd) + } + + val quadTree = Quadtree(box) { it } + + for (point in points) { + quadTree.insert(point) + } + + val selected = points[3] + val radius = 40.0 + + val nearestQuery = quadTree.nearest(selected, radius) + + val batch = drawer.rectangleBatch { + this.fill = null + this.stroke = ColorRGBa.GRAY + this.strokeWeight = 0.5 + quadTree.batch(this) + } + + extend { + drawer.clear(ColorRGBa.BLACK) + + drawer.rectangles(batch) + + drawer.fill = ColorRGBa.PINK.opacify(0.7) + drawer.stroke = null + drawer.circles(points, 5.0) + + nearestQuery?.let { (nearest, neighbours, nodes) -> + drawer.stroke = null + drawer.fill = ColorRGBa.YELLOW.opacify(0.2) + + for (node in nodes) { + node.draw(drawer) + } + + drawer.fill = ColorRGBa.GREEN.opacify(0.7) + drawer.circles(neighbours, 5.0) + + drawer.fill = ColorRGBa.RED.opacify(0.9) + drawer.circle(nearest, 5.0) + + drawer.fill = ColorRGBa.PINK + drawer.circle(selected, 5.0) + + drawer.stroke = ColorRGBa.PINK + drawer.fill = null + drawer.circle(selected, radius) + } + } + } + } +} \ No newline at end of file diff --git a/orx-quadtree/src/main/kotlin/Quadtree.kt b/orx-quadtree/src/main/kotlin/Quadtree.kt new file mode 100644 index 00000000..653a4e57 --- /dev/null +++ b/orx-quadtree/src/main/kotlin/Quadtree.kt @@ -0,0 +1,215 @@ +package quadtree + +import org.openrndr.draw.Drawer +import org.openrndr.draw.RectangleBatchBuilder +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import org.openrndr.shape.intersects + +data class QuadtreeQuery(val nearest: T, val neighbours: List, val quads: List>) + +/** + * Quadtree + * + * @param T + * @property bounds the tree's bounding box + * @property maxObjects maximum number of objects per node + * @property mapper + */ +class Quadtree(val bounds: Rectangle, val maxObjects: Int = 10, val mapper: ((T) -> Vector2)) { + /** + * The 4 nodes of the tree + */ + val nodes = arrayOfNulls>(4) + var depth = 0 + val objects = mutableListOf() + + private val isLeaf: Boolean + get() = nodes[0] == null + + /** + * Clears the whole tree + */ + fun clear() { + objects.clear() + + for (i in nodes.indices) { + nodes[i]?.let { + it.clear() + + nodes[i] = null + } + } + } + + /** + * Finds the nearest and neighbouring points within a radius + * + * @param element + * @param radius + * @return + */ + fun nearest(element: T, radius: Double): QuadtreeQuery? { + val point = mapper(element) + + if (!bounds.contains(point)) return null + + val r2 = radius * radius + + val scaledBounds = Rectangle.fromCenter(point, radius * 2) + val intersected: List> = intersect(scaledBounds) ?: return null + + var minDist = Double.MAX_VALUE + val nearestObjects = mutableListOf() + var nearestObject: T? = null + + for (interNode in intersected) { + for (obj in interNode.objects) { + if (element == obj) continue + val p = mapper(obj) + + val dist = p.squaredDistanceTo(point) + + if (dist < r2) { + nearestObjects.add(obj) + + if (dist < minDist) { + minDist = dist + nearestObject = obj + } + } + } + } + + if (nearestObject == null) return null + + return QuadtreeQuery(nearestObject, nearestObjects, intersected) + } + + /** + * Inserts the element in the appropriate node + * + * @param element + * @return + */ + fun insert(element: T): Boolean { + // only* the root needs to check this + if (depth == 0) { + if (!bounds.contains(mapper(element))) return false + } + + if ((objects.size < maxObjects && isLeaf)) { + objects.add(element) + + return true + } + + if (isLeaf) subdivide() + + objects.add(element) + + for (obj in objects) { + val p = mapper(obj) + val x = if (p.x > bounds.center.x) 1 else 0 + val y = if (p.y > bounds.center.y) 1 else 0 + val nodeIndex = x + y * 2 + + nodes[nodeIndex]?.insert(obj) + } + + objects.clear() + + return true + } + + /** + * Finds which node the element is within (but not necessarily belonging to) + * + * @param element + * @return + */ + fun findNode(element: T): Quadtree? { + val v = mapper(element) + + if (!bounds.contains(v)) return null + + if (isLeaf) return this + + for (node in nodes) { + node?.findNode(element)?.let { return it } + } + + return null + } + + /** + * Draw the quadtree using batching + * + * @param batchBuilder + */ + fun batch(batchBuilder: RectangleBatchBuilder) { + batchBuilder.rectangle(bounds) + + for (node in nodes) { + node?.batch(batchBuilder) + } + } + + /** + * Draw the quadtree + * + * @param drawer + */ + fun draw(drawer: Drawer) { + drawer.rectangle(bounds) + + for (node in nodes) { + node?.draw(drawer) + } + } + + private fun intersect(rect: Rectangle): List>? { + val intersects = intersects(bounds, rect) + + if (!intersects) return null + + if (isLeaf) return listOf(this) + + val intersected = mutableListOf>() + + for (node in nodes) { + if (node != null) node.intersect(rect)?.let { + intersected.addAll(it) + } + } + + return intersected + } + + private fun subdivide() { + val width = bounds.center.x - bounds.corner.x + val height = bounds.center.y - bounds.corner.y + + val newDepth = depth + 1 + + var node = Quadtree(Rectangle(bounds.corner, width, height), maxObjects, mapper) + node.depth = newDepth + nodes[0] = node + + node = Quadtree(Rectangle(Vector2(bounds.center.x, bounds.corner.y), width, height), maxObjects, mapper) + node.depth = newDepth + nodes[1] = node + + node = Quadtree(Rectangle(Vector2(bounds.corner.x, bounds.center.y), width, height), maxObjects, mapper) + node.depth = newDepth + nodes[2] = node + + node = Quadtree(Rectangle(bounds.center, width, height), maxObjects, mapper) + node.depth = newDepth + nodes[3] = node + } + + override fun toString(): String { + return "QuadTree { objects: ${objects.size}, depth: $depth, isLeaf: $isLeaf" + } +} \ No newline at end of file diff --git a/orx-triangulation/build.gradle b/orx-triangulation/build.gradle index 6a5d81cc..9bd99705 100644 --- a/orx-triangulation/build.gradle +++ b/orx-triangulation/build.gradle @@ -9,7 +9,7 @@ sourceSets { } def useSnapshot = false -def delaunatorVersion = (useSnapshot) ? "0.4.0-SNAPSHOT" : "1.0.1" +def delaunatorVersion = (useSnapshot) ? "0.4.0-SNAPSHOT" : "1.0.2" dependencies { implementation project(":orx-noise") diff --git a/settings.gradle b/settings.gradle index 9f727eb5..98007d91 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,7 @@ include 'openrndr-demos', 'orx-palette', 'orx-panel', 'orx-poisson-fill', + 'orx-quadtree', 'orx-rabbit-control', 'orx-realsense2', 'orx-realsense2-natives-linux-x64',