Skip to content

Commit

Permalink
[orx-hash-grid] Add generated and verified documentation, improve Dem…
Browse files Browse the repository at this point in the history
…oHashGrid01.kt
  • Loading branch information
edwinRNDR committed Jan 24, 2025
1 parent 3442c95 commit a1b4070
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 5 deletions.
86 changes: 86 additions & 0 deletions orx-hash-grid/src/commonMain/kotlin/HashGrid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand Down Expand Up @@ -62,31 +80,67 @@ 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)
}
}
}

/**
* 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<GridCoords, Cell>()

/**
* 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()
val y = (v.y / cellSize).fastFloor()
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) {
Expand All @@ -95,19 +149,51 @@ 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) }
cell.insert(point, owner)
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<Any> = emptySet()): Boolean {
val c = coords(query)
if (cells[c] == null) {
Expand Down
92 changes: 92 additions & 0 deletions orx-hash-grid/src/commonMain/kotlin/HashGrid3D.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand Down Expand Up @@ -69,24 +95,59 @@ 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)
}
}
}

/**
* 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<GridCoords3D, Cell3D>()


/**
* 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()
Expand All @@ -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) {
Expand All @@ -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
}
Expand All @@ -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<Any> = emptySet()): Boolean {
val c = coords(query)
if (cells[c] == null) {
Expand Down
7 changes: 7 additions & 0 deletions orx-hash-grid/src/jvmDemo/kotlin/DemoFilter01.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions orx-hash-grid/src/jvmDemo/kotlin/DemoFilter3D01.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 21 additions & 5 deletions orx-hash-grid/src/jvmDemo/kotlin/DemoHashGrid01.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)

}
}
}
Expand Down

0 comments on commit a1b4070

Please sign in to comment.