diff --git a/build.gradle b/build.gradle index 14177988..7b0a7fd1 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,8 @@ def multiplatformModules = [ "orx-shader-phrases", "orx-shapes", "orx-quadtree", + "orx-hash-grid" + ] def doNotPublish = ["openrndr-demos"] diff --git a/orx-hash-grid/README.md b/orx-hash-grid/README.md new file mode 100644 index 00000000..dd8ed123 --- /dev/null +++ b/orx-hash-grid/README.md @@ -0,0 +1,3 @@ +# orx-hash-grid + +A 2D space partitioning for points. \ No newline at end of file diff --git a/orx-hash-grid/build.gradle.kts b/orx-hash-grid/build.gradle.kts new file mode 100644 index 00000000..6e146c11 --- /dev/null +++ b/orx-hash-grid/build.gradle.kts @@ -0,0 +1,93 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +val kotlinxSerializationVersion: String by rootProject.extra +val kotestVersion: String by rootProject.extra +val junitJupiterVersion: String by rootProject.extra +val jvmTarget: String by rootProject.extra +val kotlinApiVersion: String by rootProject.extra +val kotlinVersion: String by rootProject.extra +val kotlinLoggingVersion: String by rootProject.extra +val kluentVersion: String by rootProject.extra +val openrndrVersion: String by rootProject.extra +val openrndrOS: String by rootProject.extra +val spekVersion: String by rootProject.extra + +kotlin { + jvm { + compilations { + val demo by creating { + defaultSourceSet { + kotlin.srcDir("src/demo") + dependencies { + implementation(project(":orx-camera")) + implementation("org.openrndr:openrndr-application:$openrndrVersion") + implementation("org.openrndr:openrndr-extensions:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion") + implementation(compilations["main"]!!.output.allOutputs) + } + } + } + } + compilations.all { + kotlinOptions.jvmTarget = jvmTarget + kotlinOptions.apiVersion = kotlinApiVersion + } + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + js(IR) { + browser() + nodejs() + } + + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + implementation(project(":orx-parameters")) + implementation(project(":orx-shader-phrases")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion") + implementation("org.openrndr:openrndr-application:$openrndrVersion") + implementation("org.openrndr:openrndr-draw:$openrndrVersion") + implementation("org.openrndr:openrndr-filter:$openrndrVersion") + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") + implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + } + } + @Suppress("UNUSED_VARIABLE") + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + implementation("io.kotest:kotest-assertions-core:$kotestVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(kotlin("test-junit5")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion") + implementation("org.spekframework.spek2:spek-dsl-jvm:$spekVersion") + implementation("org.amshove.kluent:kluent:$kluentVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} \ 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 new file mode 100644 index 00000000..52bbea38 --- /dev/null +++ b/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt @@ -0,0 +1,95 @@ +package org.openrndr.extra.hashgrid + +import org.openrndr.math.Vector2 +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt +import kotlin.random.Random + +private fun Double.fastFloor(): Int { + return if (this >= 0) this.toInt() else this.toInt() - 1 +} + +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) { + points.add(point) + xMin = min(xMin, point.x) + xMax = max(xMax, point.x) + yMin = min(yMin, point.y) + yMax = max(yMax, point.y) + } + + fun squaredDistanceTo(query: Vector2): Double { + val width = xMax - xMin + val height = yMax - yMin + val x = (xMin + xMax) / 2.0 + val y = (yMin + yMax) / 2.0 + val dx = max(abs(query.x - x) - width / 2, 0.0) + val dy = max(abs(query.y - y) - height / 2, 0.0) + return dx * dx + dy * dy + } +} + +class HashGrid(val radius: Double) { + private val cells = mutableMapOf() + val cellSize = radius / sqrt(2.0) + private inline fun coords(v: Vector2): GridCoords { + val x = (v.x / cellSize).fastFloor() + val y = (v.y / cellSize).fastFloor() + return GridCoords(x, y) + } + + fun points() = sequence { + for (cell in cells.values) { + for (point in cell.points) { + yield(point) + } + } + } + + fun random(random: Random = Random.Default) : Vector2 { + return cells.values.random(random).points.random() + } + + fun insert(point: Vector2) { + val gc = coords(point) + val cell = cells.getOrPut(gc) { Cell() } + cell.insert(point) + } + + fun isFree(query: Vector2): Boolean { + val c = coords(query) + if (cells[c] == null) { + for (j in -2..2) { + for (i in -2..2) { + if (i == 0 && j == 0) { + continue + } + val n = c.offset(i, j) + val nc = cells[n] + if (nc != null && nc.squaredDistanceTo(query) <= radius * radius) { + for (p in nc.points) { + if (p.squaredDistanceTo(query) <= radius * radius) { + return false + } + } + } + } + } + return true + } else { + return false + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 0d06fba5..5f730f04 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'openrndr-demos', 'orx-jvm:orx-git-archiver-gradle', 'orx-glslify', 'orx-gradient-descent', + 'orx-hash-grid', 'orx-integral-image', 'orx-interval-tree', 'orx-jumpflood',