[orx-shapes] Add irregular grid support and demo examples

This commit introduces `Rectangle.irregularGrid`, enabling the creation of grids with irregular spacing based on weights. New helper methods and properties for 2D rectangle lists, such as subgrid selection, bounds calculation, and random access, are also added. Additionally, two new demos showcase regular and irregular grid features.
This commit is contained in:
Edwin Jakobs
2025-02-20 16:57:14 +01:00
parent 70e87c73ce
commit a7d878a710
4 changed files with 223 additions and 2 deletions

View File

@@ -1,7 +1,64 @@
package org.openrndr.extra.shapes.primitives
import org.openrndr.shape.Rectangle
import org.openrndr.shape.bounds
import kotlin.jvm.JvmName
import kotlin.math.round
import kotlin.random.Random
/**
* Divides a rectangle into a grid of sub-rectangles with irregular spacing,
* based on the specified column and row weights. Optionally, margins can be
* applied on both the horizontal and vertical directions.
*
* @param columnWeights A list of relative weights for the columns. The size
* of this list determines the number of columns, and each weight defines
* the proportional width of the respective column.
* @param rowWeights A list of relative weights for the rows. The size of
* this list determines the number of rows, and each weight defines the
* proportional height of the respective row.
* @param marginX The horizontal margin between the edges of the main rectangle
* and the grid. Defaults to 0.0.
* @param marginY The vertical margin between the edges of the main rectangle
* and the grid. Defaults to 0.0.
* @return A list of lists, where each sublist represents a row of the grid,
* and each element within the row is a sub-rectangle corresponding to a cell
* in the grid.
*/
fun Rectangle.irregularGrid(
columnWeights: List<Double>,
rowWeights: List<Double>,
marginX: Double = 0.0,
marginY: Double = 0.0,
): List<List<Rectangle>> {
val columnWeight = columnWeights.sum()
val rowWeight = rowWeights.sum()
val columnRatios = columnWeights.map { it / columnWeight }
val rowRatios = rowWeights.map { it / rowWeight }
val us = columnRatios.scan(0.0) { acc, d -> acc + d }
val vs = rowRatios.scan(0.0) { acc, d -> acc + d }
val result = mutableListOf<MutableList<Rectangle>>()
val withMargins = this.offsetEdges(-marginX, -marginY)
for (j in 0 until vs.size - 1) {
val v0 = vs[j]
val v1 = vs[j + 1]
val row = mutableListOf<Rectangle>()
for (i in 0 until us.size - 1) {
val u0 = us[i]
val u1 = us[i + 1]
row.add(withMargins.sub(u0, v0, u1, v1))
}
result.add(row)
}
return result
}
/**
* Splits [Rectangle] into a grid of [Rectangle]s
@@ -87,7 +144,7 @@ fun Rectangle.grid(
*
* @return A new 2D list of rectangles where rows and columns are swapped.
*/
fun List<List<Rectangle>>.transpose() : List<List<Rectangle>> {
fun List<List<Rectangle>>.transpose(): List<List<Rectangle>> {
val columns = MutableList<MutableList<Rectangle>>(this[0].size) { mutableListOf() }
for (row in this) {
for ((index, column) in row.withIndex()) {
@@ -95,4 +152,88 @@ fun List<List<Rectangle>>.transpose() : List<List<Rectangle>> {
}
}
return columns
}
}
/**
* Retrieves a [Rectangle] from a two-dimensional list of [Rectangle]s based on
* the specified x and y indices.
*
* @param x The column index in the two-dimensional list.
* @param y The row index in the two-dimensional list.
* @return The [Rectangle] at the specified indices (x, y).
*/
operator fun List<List<Rectangle>>.get(x: Int, y: Int): Rectangle = this[y][x]
/**
* Retrieves a sublist of [Rectangle] objects from a two-dimensional [List] given a range of indices for rows and a specific column index.
*
* @param xRange The range of indices specifying the columns to be sliced.
* @param y The index of the row from which the sublist is retrieved.
* @return A sublist of [Rectangle] objects within the specified range of columns from the specified row.
*/
operator fun List<List<Rectangle>>.get(xRange: IntRange, y: Int): List<Rectangle> = this[y].slice(xRange)
/**
* Retrieves a list of rectangles at a specific x-coordinate for a range of y-coordinates
* from a 2D list of rectangles.
*
* @param x The x-coordinate to access within each inner list.
* @param yRange The range of y-coordinates (indices of the outer list) to retrieve rectangles from.
* @return A list of rectangles corresponding to the specified x-coordinate and y-coordinate range.
*/
operator fun List<List<Rectangle>>.get(x: Int, yRange: IntRange): List<Rectangle> = yRange.map { y -> this[y][x] }
/**
* Retrieves a subgrid from a 2D list of [Rectangle]s based on the specified ranges.
*
* @param xRange The range of x indices to include in the subgrid.
* @param yRange The range of y indices to include in the subgrid.
* @return A 2D list containing the elements of the subgrid specified by the ranges.
*/
operator fun List<List<Rectangle>>.get(xRange: IntRange, yRange: IntRange): List<List<Rectangle>> =
yRange.map { y -> xRange.map { x -> this[y][x] } }
/**
* Computes the bounding rectangle that encompasses all the rectangles contained in the lists.
*
* This property traverses the two-dimensional list structure to compute the bounds of each
* individual rectangle, ultimately returning a single [Rectangle] that encompasses
* all the rectangles in all nested lists.
*
* If the list is empty or contains no rectangles, the resulting bounds might be undefined,
* depending on the behavior of the nested bounds calculations.
*/
val List<List<Rectangle>>.bounds: Rectangle
@JvmName("getRectangleListBounds") get() {
val bounds = map { it.bounds }
return bounds.bounds
}
/**
* Selects a random [Rectangle] from a nested list of rectangles using the provided random generator.
*
* @param random An instance of [Random] used to select rectangles in a random manner.
* @return A randomly selected [Rectangle] from the nested list.
*/
fun List<List<Rectangle>>.uniform(random: Random): Rectangle {
return this.random(random).random(random)
}
/**
* Retrieves a column of rectangles from a 2D list of rectangles.
*
* @param index The index of the column to retrieve.
* @return A list of [Rectangle] objects representing the specified column.
*/
fun List<List<Rectangle>>.column(index: Int): List<Rectangle> = this.map { it[index] }
/**
* Retrieves the row from a 2D list of `Rectangle` objects at the specified index.
*
* @receiver The 2D list of `Rectangle` objects.
* @param index The index of the row to retrieve. Must be in the valid range of indices for the list.
* @return A list of `Rectangle` objects representing the row at the given index.
*/
fun List<List<Rectangle>>.row(index: Int): List<Rectangle> = this[index]

View File

@@ -0,0 +1,39 @@
package primitives
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.shapes.primitives.bounds
import org.openrndr.extra.shapes.primitives.grid
import org.openrndr.extra.shapes.primitives.get
import org.openrndr.shape.bounds
fun main() = application {
configure {
width = 720
height = 720
}
program {
val grid = drawer.bounds.grid(12, 5)
extend {
drawer.stroke = ColorRGBa.WHITE
drawer.fill = null
drawer.rectangles(grid.flatten())
drawer.fill = ColorRGBa.GRAY.shade(0.4).opacify(0.5)
drawer.rectangle(grid[1..10, 0..4].bounds)
drawer.fill = ColorRGBa.PINK.shade(0.5).opacify(0.5)
drawer.rectangle(grid[5..6, 1].bounds)
drawer.fill = ColorRGBa.PINK.opacify(0.5)
drawer.rectangle(grid[2..5, 2].bounds)
drawer.fill = ColorRGBa.GRAY.opacify(0.5)
drawer.rectangle(grid[6..9, 2].bounds)
drawer.fill = ColorRGBa.GRAY.shade(0.5).opacify(0.5)
drawer.rectangle(grid[5..6, 3].bounds)
}
}
}

View File

@@ -0,0 +1,39 @@
package primitives
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.color.presets.CORAL
import org.openrndr.extra.noise.uniform
import org.openrndr.extra.noise.uniforms
import org.openrndr.extra.shapes.primitives.column
import org.openrndr.extra.shapes.primitives.irregularGrid
import org.openrndr.extra.shapes.primitives.row
import kotlin.random.Random
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend {
val r = Random(100)
val grid = drawer.bounds.irregularGrid(
Double.uniforms(13, 0.1, 0.5, r),
Double.uniforms(13, 0.1, 0.5, r),
20.0, 20.0
)
drawer.fill = null
drawer.stroke = ColorRGBa.WHITE
drawer.rectangles(grid.flatten())
drawer.stroke = ColorRGBa.BLACK
drawer.fill = ColorRGBa.PINK.opacify(0.5)
drawer.rectangles(grid.column(2))
drawer.fill = ColorRGBa.CORAL.opacify(0.5)
drawer.rectangles(grid.row(6))
}
}
}

View File

@@ -5,8 +5,10 @@ import org.openrndr.color.ColorRGBa
import org.openrndr.draw.font.loadFace
import org.openrndr.extra.shapes.bounds.bounds
import org.openrndr.extra.shapes.text.shapesFromText
import org.openrndr.internal.Driver
fun main() = application {
System.setProperty("org.openrndr.draw.wait_for_finish", "true")
configure {
width = 720
height = 720