[orx-quadtree] add quadtree module (#174)
This commit is contained in:
22
orx-quadtree/README.md
Normal file
22
orx-quadtree/README.md
Normal file
@@ -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<Vector2>(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)
|
||||
19
orx-quadtree/build.gradle
Normal file
19
orx-quadtree/build.gradle
Normal file
@@ -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)
|
||||
}
|
||||
55
orx-quadtree/src/demo/kotlin/DemoQuadTree01.kt
Normal file
55
orx-quadtree/src/demo/kotlin/DemoQuadTree01.kt
Normal file
@@ -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<Vector2>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
orx-quadtree/src/demo/kotlin/DemoQuadTree02.kt
Normal file
82
orx-quadtree/src/demo/kotlin/DemoQuadTree02.kt
Normal file
@@ -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<Vector2>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
orx-quadtree/src/main/kotlin/Quadtree.kt
Normal file
215
orx-quadtree/src/main/kotlin/Quadtree.kt
Normal file
@@ -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<T>(val nearest: T, val neighbours: List<T>, val quads: List<Quadtree<T>>)
|
||||
|
||||
/**
|
||||
* Quadtree
|
||||
*
|
||||
* @param T
|
||||
* @property bounds the tree's bounding box
|
||||
* @property maxObjects maximum number of objects per node
|
||||
* @property mapper
|
||||
*/
|
||||
class Quadtree<T>(val bounds: Rectangle, val maxObjects: Int = 10, val mapper: ((T) -> Vector2)) {
|
||||
/**
|
||||
* The 4 nodes of the tree
|
||||
*/
|
||||
val nodes = arrayOfNulls<Quadtree<T>>(4)
|
||||
var depth = 0
|
||||
val objects = mutableListOf<T>()
|
||||
|
||||
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<T>? {
|
||||
val point = mapper(element)
|
||||
|
||||
if (!bounds.contains(point)) return null
|
||||
|
||||
val r2 = radius * radius
|
||||
|
||||
val scaledBounds = Rectangle.fromCenter(point, radius * 2)
|
||||
val intersected: List<Quadtree<T>> = intersect(scaledBounds) ?: return null
|
||||
|
||||
var minDist = Double.MAX_VALUE
|
||||
val nearestObjects = mutableListOf<T>()
|
||||
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<T>? {
|
||||
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<Quadtree<T>>? {
|
||||
val intersects = intersects(bounds, rect)
|
||||
|
||||
if (!intersects) return null
|
||||
|
||||
if (isLeaf) return listOf(this)
|
||||
|
||||
val intersected = mutableListOf<Quadtree<T>>()
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user