[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 useSnapshot = false
|
||||||
def delaunatorVersion = (useSnapshot) ? "0.4.0-SNAPSHOT" : "1.0.1"
|
def delaunatorVersion = (useSnapshot) ? "0.4.0-SNAPSHOT" : "1.0.2"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":orx-noise")
|
implementation project(":orx-noise")
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ include 'openrndr-demos',
|
|||||||
'orx-palette',
|
'orx-palette',
|
||||||
'orx-panel',
|
'orx-panel',
|
||||||
'orx-poisson-fill',
|
'orx-poisson-fill',
|
||||||
|
'orx-quadtree',
|
||||||
'orx-rabbit-control',
|
'orx-rabbit-control',
|
||||||
'orx-realsense2',
|
'orx-realsense2',
|
||||||
'orx-realsense2-natives-linux-x64',
|
'orx-realsense2-natives-linux-x64',
|
||||||
|
|||||||
Reference in New Issue
Block a user