From 8ce76680b15c978e646cdfedd35b4156f29e22ef Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Wed, 25 Jan 2023 00:16:06 +0100 Subject: [PATCH] [orx-hash-grid] Add documentation, demos and improve API --- orx-hash-grid/README.md | 28 +++++- orx-hash-grid/build.gradle.kts | 10 ++ .../src/commonMain/kotlin/HashGrid.kt | 94 ++++++++++++++++--- .../src/jvmDemo/kotlin/DemoFilter01.kt | 23 +++++ .../src/jvmDemo/kotlin/DemoHashGrid01.kt | 28 ++++++ 5 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt create mode 100644 orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt diff --git a/orx-hash-grid/README.md b/orx-hash-grid/README.md index dd8ed123..82aa6a83 100644 --- a/orx-hash-grid/README.md +++ b/orx-hash-grid/README.md @@ -1,3 +1,29 @@ # orx-hash-grid -A 2D space partitioning for points. \ No newline at end of file +A 2D space partitioning for points. + +## Usage + +Create a hash grid for a given radius. +```kotlin +val grid = HashGrid(radius) +``` + +Check for a given query point if the grid is free, i.e. there is no point in the grid at distance less than `radius` away from the +query point. + +```kotlin +grid.isFree(query) +``` + +Add a point to the hash grid structure: +```kotlin +grid.insert(point) +``` + +Iterate over all points in the hash grid: +```kotlin +for (point in grid.points()) { + // do something with point +} +``` \ No newline at end of file diff --git a/orx-hash-grid/build.gradle.kts b/orx-hash-grid/build.gradle.kts index 93187500..bae8b4a1 100644 --- a/orx-hash-grid/build.gradle.kts +++ b/orx-hash-grid/build.gradle.kts @@ -15,5 +15,15 @@ kotlin { implementation(libs.kotlin.reflect) } } + + @Suppress("UNUSED_VARIABLE") + val jvmDemo by getting { + dependencies { + implementation(project(":orx-color")) + implementation(project(":orx-fx")) + implementation(project(":orx-noise")) + } + } + } } \ No newline at end of file diff --git a/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt b/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt index 0ad666c0..2b666e44 100644 --- a/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt +++ b/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt @@ -1,6 +1,7 @@ package org.openrndr.extra.hashgrid import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -15,14 +16,33 @@ private data class GridCoords(val x: Int, val y: Int) { fun offset(i: Int, j: Int): GridCoords = copy(x = x + i, y = y + j) } -private class Cell( - var xMin: Double = Double.POSITIVE_INFINITY, - var xMax: Double = Double.NEGATIVE_INFINITY, - var yMin: Double = Double.POSITIVE_INFINITY, - var yMax: Double = Double.NEGATIVE_INFINITY, -) { - val points = mutableListOf>() - fun insert(point: Vector2, owner: Any?) { +class Cell(val x: Int, val y: Int, val cellSize: Double) { + var xMin: Double = Double.POSITIVE_INFINITY + private set + var xMax: Double = Double.NEGATIVE_INFINITY + private set + var yMin: Double = Double.POSITIVE_INFINITY + private set + var yMax: Double = Double.NEGATIVE_INFINITY + private set + + val bounds: Rectangle + get() { + return Rectangle(x * cellSize, y * cellSize, cellSize, cellSize) + } + + val contentBounds: Rectangle + get() { + if (points.isEmpty()) { + return Rectangle.EMPTY + } else { + return Rectangle(xMin, yMin, xMax - xMin, yMax - yMin) + } + } + + + internal val points = mutableListOf>() + internal fun insert(point: Vector2, owner: Any?) { points.add(Pair(point, owner)) xMin = min(xMin, point.x) xMax = max(xMax, point.x) @@ -30,7 +50,7 @@ private class Cell( yMax = max(yMax, point.y) } - fun squaredDistanceTo(query: Vector2): Double { + internal fun squaredDistanceTo(query: Vector2): Double { val width = xMax - xMin val height = yMax - yMin val x = (xMin + xMax) / 2.0 @@ -39,10 +59,25 @@ private class Cell( val dy = max(abs(query.y - y) - height / 2, 0.0) return dx * dx + dy * dy } + + fun points() = sequence { + for (point in points) { + yield(point) + } + } } class HashGrid(val radius: Double) { private val cells = mutableMapOf() + fun cells() = sequence { + for (cell in cells.values) { + yield(cell) + } + } + + var size: Int = 0 + private set + val cellSize = radius / sqrt(2.0) private inline fun coords(v: Vector2): GridCoords { val x = (v.x / cellSize).fastFloor() @@ -58,17 +93,20 @@ class HashGrid(val radius: Double) { } } - fun random(random: Random = Random.Default) : Vector2 { + fun random(random: Random = Random.Default): Vector2 { return cells.values.random(random).points.random().first } - fun insert(point: Vector2, owner:Any? = null) { + fun insert(point: Vector2, owner: Any? = null) { val gc = coords(point) - val cell = cells.getOrPut(gc) { Cell() } + val cell = cells.getOrPut(gc) { Cell(gc.x, gc.y, cellSize) } cell.insert(point, owner) + size += 1 } - fun isFree(query: Vector2, ignoreOwners:Set = emptySet()): Boolean { + fun cell(query: Vector2): Cell? = cells[coords(query)] + + fun isFree(query: Vector2, ignoreOwners: Set = emptySet()): Boolean { val c = coords(query) if (cells[c] == null) { for (j in -2..2) { @@ -95,4 +133,34 @@ class HashGrid(val radius: Double) { return cells[c]!!.points.all { it.second != null && it.second in ignoreOwners } } } +} + +/** + * Construct a hash grid containing all points in the list + * @param radius radius of the hash grid + */ +fun List.hashGrid(radius: Double): HashGrid { + val grid = HashGrid(radius) + for (point in this) { + grid.insert(point) + } + return grid +} + +/** + * Return a list that only contains points at a minimum distance. + * @param radius the minimum distance between any two points in the returned list + */ +fun List.filter(radius: Double): List { + return if (size <= 1) { + this + } else { + val grid = HashGrid(radius) + for (point in this) { + if (grid.isFree(point)) { + grid.insert(point) + } + } + grid.points().map { it.first }.toList() + } } \ No newline at end of file diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt new file mode 100644 index 00000000..880b2509 --- /dev/null +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt @@ -0,0 +1,23 @@ +import org.openrndr.application +import org.openrndr.extra.hashgrid.filter +import org.openrndr.extra.noise.uniform +import kotlin.random.Random + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + val r = Random(0) + val points = (0 until 10000).map { + drawer.bounds.uniform(random = r) + } + val filteredPoints = points.filter(20.0) + extend { + drawer.circles(filteredPoints, 4.0) + } + } + } +} \ No newline at end of file diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt new file mode 100644 index 00000000..d6f55f72 --- /dev/null +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt @@ -0,0 +1,28 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.hashgrid.HashGrid +import org.openrndr.extra.noise.uniform +import kotlin.random.Random + +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + val r = Random(0) + val hashGrid = HashGrid(20.0) + extend { + val p = drawer.bounds.uniform(random = r) + if (hashGrid.isFree(p)) { + hashGrid.insert(p) + } + drawer.circles(hashGrid.points().map { it.first }.toList(), 4.0) + drawer.fill = null + drawer.stroke = ColorRGBa.WHITE + drawer.rectangles(hashGrid.cells().map { it.bounds }.toList()) + } + } + } +} \ No newline at end of file