From a7d878a710972f467d6763255a03d1c0f0ccac7a Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Thu, 20 Feb 2025 16:57:14 +0100 Subject: [PATCH] [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. --- .../kotlin/primitives/RectangleGrid.kt | 145 +++++++++++++++++- .../kotlin/primitives/DemoRectangleGrid03.kt | 39 +++++ .../primitives/DemoRectangleIrregularGrid.kt | 39 +++++ .../src/jvmDemo/kotlin/text/DemoText01.kt | 2 + 4 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleGrid03.kt create mode 100644 orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleIrregularGrid.kt diff --git a/orx-shapes/src/commonMain/kotlin/primitives/RectangleGrid.kt b/orx-shapes/src/commonMain/kotlin/primitives/RectangleGrid.kt index 42c96a77..9504c9ec 100644 --- a/orx-shapes/src/commonMain/kotlin/primitives/RectangleGrid.kt +++ b/orx-shapes/src/commonMain/kotlin/primitives/RectangleGrid.kt @@ -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, + rowWeights: List, + marginX: Double = 0.0, + marginY: Double = 0.0, +): List> { + + 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>() + + 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() + 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>.transpose() : List> { +fun List>.transpose(): List> { val columns = MutableList>(this[0].size) { mutableListOf() } for (row in this) { for ((index, column) in row.withIndex()) { @@ -95,4 +152,88 @@ fun List>.transpose() : List> { } } return columns -} \ No newline at end of file +} + +/** + * 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>.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>.get(xRange: IntRange, y: Int): List = 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>.get(x: Int, yRange: IntRange): List = 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>.get(xRange: IntRange, yRange: IntRange): List> = + 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>.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>.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>.column(index: Int): List = 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>.row(index: Int): List = this[index] \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleGrid03.kt b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleGrid03.kt new file mode 100644 index 00000000..56cc8b83 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleGrid03.kt @@ -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) + } + } +} diff --git a/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleIrregularGrid.kt b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleIrregularGrid.kt new file mode 100644 index 00000000..16dd778e --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleIrregularGrid.kt @@ -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)) + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/text/DemoText01.kt b/orx-shapes/src/jvmDemo/kotlin/text/DemoText01.kt index e27371cb..506c13ba 100644 --- a/orx-shapes/src/jvmDemo/kotlin/text/DemoText01.kt +++ b/orx-shapes/src/jvmDemo/kotlin/text/DemoText01.kt @@ -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