diff --git a/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt b/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt index e09f2dba..de85d910 100644 --- a/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt +++ b/orx-hash-grid/src/commonMain/kotlin/HashGrid.kt @@ -18,6 +18,13 @@ private data class GridCoords(val x: Int, val y: Int) { fun offset(i: Int, j: Int): GridCoords = copy(x = x + i, y = y + j) } +/** + * Represents a cell in a 2D space, defined by its position and size. + * + * @property x The x-coordinate of the cell in the grid. + * @property y The y-coordinate of the cell in the grid. + * @property cellSize The size of the cell along each axis. + */ class Cell(val x: Int, val y: Int, val cellSize: Double) { var xMin: Double = Double.POSITIVE_INFINITY private set @@ -28,11 +35,22 @@ class Cell(val x: Int, val y: Int, val cellSize: Double) { var yMax: Double = Double.NEGATIVE_INFINITY private set + /** + * Calculates and returns the rectangular bounds of the cell in the 2D grid. + * The bounds are represented as a rectangle with its top-left position and size derived + * from the cell's position (`x`, `y`) and `cellSize`. + */ val bounds: Rectangle get() { return Rectangle(x * cellSize, y * cellSize, cellSize, cellSize) } + /** + * Computes the bounds of the content within the cell, considering the points stored in it. + * If no points are present in the cell, the bounds will be represented as an empty rectangle. + * Otherwise, the bounds are determined by the minimum and maximum x and y coordinates + * among the points in the cell. + */ val contentBounds: Rectangle get() { if (points.isEmpty()) { @@ -62,6 +80,12 @@ class Cell(val x: Int, val y: Int, val cellSize: Double) { return dx * dx + dy * dy } + /** + * Generates a sequence of points contained within the current cell. + * Iterates over the points stored in the cell and yields each point one by one. + * + * @return A sequence of points in the cell. + */ fun points() = sequence { for (point in points) { yield(point) @@ -69,17 +93,39 @@ class Cell(val x: Int, val y: Int, val cellSize: Double) { } } +/** + * Represents a 2D spatial hash grid used for efficiently managing and querying points in a sparse space. + * + * @property radius The maximum distance between points for them to be considered neighbors. + */ class HashGrid(val radius: Double) { private val cells = mutableMapOf() + + /** + * Returns a sequence of all cells stored in the grid. + * Iterates through the values in the internal `cells` map and yields each cell. + */ fun cells() = sequence { for (cell in cells.values) { yield(cell) } } + /** + * Represents the total number of elements (points or data) that are currently stored in the grid. + * + * This property is managed internally and reflects the current size of the grid data structure. + * It cannot be modified directly from outside the class. + */ var size: Int = 0 private set + /** + * Represents the size of a single cell in the hash grid. + * + * Computed as the radius divided by the square root of 2. + * This value determines the spatial resolution of each cell in the grid. + */ val cellSize = radius / sqrt(2.0) private fun coords(v: Vector2): GridCoords { val x = (v.x / cellSize).fastFloor() @@ -87,6 +133,14 @@ class HashGrid(val radius: Double) { return GridCoords(x, y) } + /** + * Generates a sequence of all points stored within the grid. + * + * Iterates through each cell in the grid's `cells` map, yielding all points + * contained within each cell. + * + * @return A sequence of points from all cells in the grid. + */ fun points() = sequence { for (cell in cells.values) { for (point in cell.points) { @@ -95,10 +149,24 @@ class HashGrid(val radius: Double) { } } + /** + * Selects a random point from the grid using the provided random number generator. + * + * @param random The random number generator to use. Defaults to `Random.Default`. + * @return A randomly selected point, represented as a `Vector2`, from the grid's cells. + */ fun random(random: Random = Random.Default): Vector2 { return cells.values.random(random).points.random().first } + /** + * Inserts a point into the grid, associating it with an owner if provided. + * The method calculates the grid cell corresponding to the provided point and inserts + * the point into that cell. If the cell does not exist, it is created. + * + * @param point The point to insert, represented as a `Vector2` object. + * @param owner An optional object to associate with the point. Defaults to `null` if no owner is specified. + */ fun insert(point: Vector2, owner: Any? = null) { val gc = coords(point) val cell = cells.getOrPut(gc) { Cell(gc.x, gc.y, cellSize) } @@ -106,8 +174,26 @@ class HashGrid(val radius: Double) { size += 1 } + /** + * Retrieves the cell corresponding to the given query point in the grid. + * The method calculates the grid coordinates for the query point and returns + * the cell found at those coordinates, if it exists. + * + * @param query The point in 2D space, represented as a `Vector2`, for which + * to retrieve the corresponding cell. + * @return The `Cell` corresponding to the given query point, or `null` if + * no cell exists at the calculated coordinates. + */ fun cell(query: Vector2): Cell? = cells[coords(query)] + /** + * Checks if a specific query point in 2D space is free from any nearby points or owners, + * according to the internal grid structure and other constraints. + * + * @param query The 2D point represented as a Vector2 to check for available space. + * @param ignoreOwners A set of owners to be ignored while checking for nearby points. Defaults to an empty set. + * @return `true` if the query point is free, `false` otherwise. + */ fun isFree(query: Vector2, ignoreOwners: Set = emptySet()): Boolean { val c = coords(query) if (cells[c] == null) { diff --git a/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt b/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt index 60dd34fe..1b148ade 100644 --- a/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt +++ b/orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt @@ -17,6 +17,16 @@ private data class GridCoords3D(val x: Int, val y: Int, val z: Int) { fun offset(i: Int, j: Int, k : Int): GridCoords3D = copy(x = x + i, y = y + j, z = z + k) } +/** + * Represents a 3D cell with a fixed size in a spatial hash grid structure. A `Cell3D` is aligned + * along a grid using its integer coordinates and supports operations to manage points within + * its bounds, calculate distances to a query point, and retrieve its own bounding boxes. + * + * @property x The x-coordinate of the cell within the grid. + * @property y The y-coordinate of the cell within the grid. + * @property z The z-coordinate of the cell within the grid. + * @property cellSize The size of the cell in all dimensions. + */ class Cell3D(val x: Int, val y: Int, val z: Int, val cellSize: Double) { var xMin: Double = Double.POSITIVE_INFINITY private set @@ -31,11 +41,27 @@ class Cell3D(val x: Int, val y: Int, val z: Int, val cellSize: Double) { var zMax: Double = Double.NEGATIVE_INFINITY private set + /** + * Represents the 3D bounding box of the cell. + * + * The bounds are calculated based on the cell's position (`x`, `y`, `z`) and + * the uniform size of the cell (`cellSize`). It defines a cuboid in 3D space + * with its origin at `(x * cellSize, y * cellSize, z * cellSize)` and dimensions + * defined by `cellSize` along all three axes. + * + * @return A `Box` representing the spatial boundary of the cell. + */ val bounds: Box get() { return Box(Vector3(x * cellSize, y * cellSize, z * cellSize), cellSize, cellSize, cellSize) } + /** + * Provides the bounding 3D box that contains all the points within the cell. + * If the `points` collection is empty, it returns an empty box. Otherwise, + * it calculates the bounding box based on the minimum and maximum coordinates + * of the stored points (`xMin`, `xMax`, `yMin`, `yMax`, `zMin`, `zMax`). + */ val contentBounds: Box get() { return if (points.isEmpty()) { @@ -69,6 +95,14 @@ class Cell3D(val x: Int, val y: Int, val z: Int, val cellSize: Double) { return dx * dx + dy * dy + dz * dz } + /** + * Generates a sequence of all the points stored in the `points` collection. + * + * This method iterates over the `points` collection and yields each element. + * Useful for lazily accessing the points in the order they are stored. + * + * @return A sequence of points contained within the `points` collection. + */ fun points() = sequence { for (point in points) { yield(point) @@ -76,17 +110,44 @@ class Cell3D(val x: Int, val y: Int, val z: Int, val cellSize: Double) { } } +/** + * Represents a 3D Hash Grid structure used for spatial partitioning of points in 3D space. + * This structure organizes points into grid-based cells, enabling efficient spatial querying + * and insertion operations. + * + * @property radius The radius used to determine proximity checks within the grid. + * Points are considered neighbors if their spatial distance is less than or equal to this radius. + */ class HashGrid3D(val radius: Double) { private val cells = mutableMapOf() + + + /** + * Returns a sequence of all the cells present in the hash grid. + * Each cell is yielded individually from the internal mapping. + */ fun cells() = sequence { for (cell in cells.values) { yield(cell) } } + /** + * Represents the total number of points currently stored in the hash grid. + * This property is incremented whenever a new point is inserted into the grid. + * It's read-only for external access and cannot be modified outside the class. + */ var size: Int = 0 private set + /** + * The size of a single cell in the 3D hash grid. + * + * The cell size is computed as the radius of the grid divided by the square root of 3, + * which ensures that the cell dimensions are scaled appropriately in a 3D space. + * This value influences the spatial resolution of the grid and determines + * how points are grouped into cells during computations such as insertion or querying. + */ val cellSize = radius / sqrt(3.0) private fun coords(v: Vector3): GridCoords3D { val x = (v.x / cellSize).fastFloor() @@ -95,6 +156,14 @@ class HashGrid3D(val radius: Double) { return GridCoords3D(x, y, z) } + /** + * Returns a sequence of all points contained in the hash grid. + * + * Iterates over all cells in the grid and yields each contained point. + * Each point is represented as a value yielded by the sequence. + * + * @return A sequence of all points stored in the hash grid. + */ fun points() = sequence { for (cell in cells.values) { for (point in cell.points) { @@ -103,6 +172,12 @@ class HashGrid3D(val radius: Double) { } } + /** + * Selects a random 3D vector from the points stored in the hash grid. + * + * @param random A random number generator to use for selection. Defaults to `Random.Default`. + * @return A randomly selected `Vector3` from the hash grid. + */ fun random(random: Random = Random.Default): Vector3 { return cells.values.random(random).points.random().first } @@ -114,8 +189,25 @@ class HashGrid3D(val radius: Double) { size += 1 } + /** + * Retrieves the 3D cell corresponding to the given query point in the spatial hash grid. + * + * This method computes the grid coordinates of the query vector and attempts to fetch + * the corresponding cell from the internal cell mapping. + * + * @param query A `Vector3` object representing the point used to locate the corresponding cell. + * @return A `Cell3D` object if a cell exists for the given query point, or `null` if no such cell is found. + */ fun cell(query: Vector3): Cell3D? = cells[coords(query)] + /** + * Determines whether a specific point in the 3D grid is free, considering the proximity + * to other points and optionally ignoring specified owners. + * + * @param query The `Vector3` representing the point to check for availability. + * @param ignoreOwners A set of owners to ignore during the proximity check. Default is an empty set. + * @return `true` if the point is considered free or not occupied; otherwise, `false`. + */ fun isFree(query: Vector3, ignoreOwners: Set = emptySet()): Boolean { val c = coords(query) if (cells[c] == null) { diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt index 9c39067a..959e27be 100644 --- a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt @@ -3,6 +3,13 @@ import org.openrndr.extra.hashgrid.filter import org.openrndr.extra.noise.shapes.uniform import kotlin.random.Random +/** A demo to generate and display filtered random points. + * + * The program performs the following steps: + * - Generates 10,000 random points uniformly distributed within the drawable bounds. + * - Filters the generated points to enforce a minimum distance of 20.0 units between them. + * - Visualizes the filtered points as circles with a radius of 10.0 units on the canvas. + */ fun main() { application { configure { diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt index e2e21625..192ea0ba 100644 --- a/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt @@ -10,6 +10,16 @@ import org.openrndr.extra.noise.uniformRing import org.openrndr.math.Vector3 import kotlin.random.Random +/** + * This demo sets up and renders a 3D visualization of filtered random points displayed as small spheres. + * + * The program performs the following key steps: + * - Generates 10,000 random 3D points within a ring defined by a minimum and maximum radius. + * - Filters the points to ensure a minimum distance between any two points using a spatial hash grid. + * - Creates a small sphere mesh that will be instanced for each filtered point. + * - Sets up an orbital camera to allow viewing the 3D scene interactively. + * - Renders the filtered points by translating the sphere mesh to each point's position and applying a shader that modifies the fragment color based on the view normal. + */ fun main() = application { configure { width = 720 diff --git a/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt b/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt index 598f4d94..fbe228fa 100644 --- a/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt +++ b/orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt @@ -4,6 +4,15 @@ import org.openrndr.extra.hashgrid.HashGrid import org.openrndr.extra.noise.shapes.uniform import kotlin.random.Random +/** + * This demo sets up an interactive graphics application with a configurable + * display window and visualization logic. It uses a `HashGrid` to manage points + * in a 2D space and randomly generates points within the drawable area. These + * points are then inserted into the grid if they satisfy certain spatial conditions. + * The visual output includes: + * - Rectangles representing the bounds of the cells in the grid. + * - Circles representing the generated points. + */ fun main() { application { configure { @@ -12,16 +21,23 @@ fun main() { } program { val r = Random(0) - val hashGrid = HashGrid(20.0) + val hashGrid = HashGrid(72.0) + extend { - val p = drawer.bounds.uniform(random = r) - if (hashGrid.isFree(p)) { - hashGrid.insert(p) + for (i in 0 until 100) { + 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()) + drawer.fill = null + drawer.stroke = ColorRGBa.PINK + drawer.circles(hashGrid.points().map { it.first }.toList(), 36.0) + } } }