android 去除 orx 相关依赖

This commit is contained in:
2025-11-26 17:00:11 +08:00
parent 2525d30c80
commit 0c90073363
25 changed files with 3653 additions and 895 deletions

View File

@@ -51,19 +51,10 @@ dependencies {
implementation(libs.androidx.constraintlayout)
implementation(libs.mapbox.maps)
implementation(project(":math"))
implementation(project(":orx-triangulation")) {
exclude(group = "org.openrndr", module = "openrndr-draw")
}
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(project(":icegps-common"))
implementation(project(":icegps-shared"))
implementation(project(":orx-marching-squares"))
implementation(project(":orx-palette")) {
exclude(group = "org.openrndr", module = "openrndr-draw")
}
implementation(project(":orx-shapes")) {
exclude(group = "org.openrndr", module = "openrndr-draw")
}
implementation(project(":icegps-triangulation"))
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)

View File

@@ -4,14 +4,19 @@ import ColorBrewer2Type
import android.content.Context
import android.util.Log
import colorBrewer2Palettes
import com.icegps.math.geometry.Rectangle
import com.icegps.math.geometry.Vector2D
import com.icegps.math.geometry.Vector3D
import com.icegps.orx.catmullrom.CatmullRomChain2
import com.icegps.orx.ktx.area
import com.icegps.orx.ktx.toColorInt
import com.icegps.orx.ktx.toMapboxPoint
import com.icegps.orx.ktx.toast
import com.icegps.orx.triangulation.DelaunayTriangulation3D
import com.icegps.orx.triangulation.Triangle3D
import com.icegps.orx.marchingsquares.ShapeContour
import com.icegps.orx.marchingsquares.findContours
import com.icegps.shared.ktx.TAG
import com.icegps.triangulation.DelaunayTriangulation
import com.icegps.triangulation.Triangle
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.LineString
@@ -35,11 +40,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.openrndr.extra.shapes.splines.CatmullRomChain2
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Rectangle
import org.openrndr.shape.ShapeContour
import kotlin.math.max
class ContoursManager(
@@ -136,7 +136,7 @@ class ContoursManager(
}
}
private var triangles: List<Triangle3D> = listOf()
private var triangles: List<Triangle> = listOf()
private var isTriangleVisible: Boolean = true
fun setTriangleVisible(visible: Boolean) {
@@ -170,10 +170,9 @@ class ContoursManager(
val zip = (0..contourSize).map { index ->
heightRange.start + index * step
}.zipWithNext { a, b -> a..b }
val points = points.map { Vector3(it.x, it.y, it.z) }
val area = points.area
val triangulation = DelaunayTriangulation3D(points)
val triangles: MutableList<Triangle3D> = triangulation.triangles()
val triangulation = DelaunayTriangulation(points)
val triangles = triangulation.triangles()
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
} else {
@@ -228,12 +227,12 @@ class ContoursManager(
}
fun findContours(
triangles: MutableList<Triangle3D>,
triangles: List<Triangle>,
range: ClosedFloatingPointRange<Double>,
area: Rectangle,
cellSize: Double
): List<ShapeContour> {
return org.openrndr.extra.marchingsquares.findContours(
return findContours(
f = { v ->
val triangle = triangles.firstOrNull { triangle ->
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
@@ -370,7 +369,7 @@ class ContoursManager(
}
}
fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
fun isPointInTriangle3D(point: Vector2D, triangle: List<Vector3D>): Boolean {
require(triangle.size == 3) { "三角形必须有3个顶点" }
val (v1, v2, v3) = triangle
@@ -395,15 +394,15 @@ fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
* @param triangle 三角形的三个顶点
* @return 三维点 (x, y, z)
*/
fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
fun interpolateHeight(point: Vector2D, triangle: List<Vector3D>): Vector3D {
/**
* 计算点在三角形中的重心坐标
*/
fun calculateBarycentricCoordinates(
point: Vector2,
v1: Vector3,
v2: Vector3,
v3: Vector3
point: Vector2D,
v1: Vector3D,
v2: Vector3D,
v3: Vector3D
): Triple<Double, Double, Double> {
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
@@ -424,5 +423,5 @@ fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
// 使用重心坐标插值z值
val z = alpha * v1.z + beta * v2.z + gamma * v3.z
return Vector3(point.x, point.y, z)
return Vector3D(point.x, point.y, z)
}

View File

@@ -1,8 +1,8 @@
package com.icegps.orx
import com.icegps.math.geometry.Vector2D
import com.icegps.orx.triangulation.DelaunayTriangulation3D
import org.openrndr.math.Vector3
import com.icegps.math.geometry.Vector3D
import com.icegps.triangulation.DelaunayTriangulation
import kotlin.math.absoluteValue
import kotlin.math.ceil
@@ -29,11 +29,11 @@ data class GridModel(
}
fun triangulationToGrid(
delaunator: DelaunayTriangulation3D,
delaunator: DelaunayTriangulation,
cellSize: Double = 50.0,
maxSidePixels: Int = 5000
): GridModel {
fun pointInTriangle(pt: Vector2D, a: Vector3, b: Vector3, c: Vector3): Boolean {
fun pointInTriangle(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D): Boolean {
val v0x = c.x - a.x
val v0y = c.y - a.y
val v1x = b.x - a.x
@@ -55,11 +55,11 @@ fun triangulationToGrid(
return u >= 0 && v >= 0 && u + v <= 1
}
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3, b: Vector3, c: Vector3, values: DoubleArray): Double {
val area = { p1: Vector2D, p2: Vector3, p3: Vector3 ->
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D, values: DoubleArray): Double {
val area = { p1: Vector2D, p2: Vector3D, p3: Vector3D ->
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
}
val area2 = { p1: Vector3, p2: Vector3, p3: Vector3 ->
val area2 = { p1: Vector3D, p2: Vector3D, p3: Vector3D ->
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
}
val areaTotal = area2(a, b, c)

View File

@@ -15,7 +15,6 @@ import com.mapbox.maps.extension.style.layers.properties.generated.LineCap
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import org.openrndr.math.YPolarity
class PolylineManager(
private val mapView: MapView
@@ -99,7 +98,6 @@ class PolylineManager(
fun fromPoints(
points: List<Vector3D>,
closed: Boolean,
polarity: YPolarity = YPolarity.CW_NEGATIVE_Y
) = if (points.isEmpty()) {
emptyList()
} else {

View File

@@ -0,0 +1,135 @@
package com.icegps.orx.catmullrom
import com.icegps.math.geometry.Vector2D
import com.icegps.orx.marchingsquares.Segment2D
import com.icegps.orx.marchingsquares.ShapeContour
import kotlin.math.min
import kotlin.math.pow
private const val almostZero = 0.00000001
private const val almostOne = 0.99999999
/**
* Creates a 2D Catmull-Rom spline curve.
*
* Can be represented as a segment drawn between [p1] and [p2],
* while [p0] and [p3] are used as control points.
*
* Under some circumstances alpha can have
* no perceptible effect, for example,
* when creating closed shapes with the vertices
* forming a regular 2D polygon.
*
* @param p0 The first control point.
* @param p1 The starting anchor point.
* @param p2 The ending anchor point.
* @param p3 The second control point.
* @param alpha The *tension* of the curve.
* Use `0.0` for the uniform spline, `0.5` for the centripetal spline, `1.0` for the chordal spline.
*/
class CatmullRom2(val p0: Vector2D, val p1: Vector2D, val p2: Vector2D, val p3: Vector2D, val alpha: Double = 0.5) {
/** Value of t for p0. */
val t0: Double = 0.0
/** Value of t for p1. */
val t1: Double = calculateT(t0, p0, p1)
/** Value of t for p2. */
val t2: Double = calculateT(t1, p1, p2)
/** Value of t for p3. */
val t3: Double = calculateT(t2, p2, p3)
fun position(rt: Double): Vector2D {
val t = t1 + rt * (t2 - t1)
val a1 = p0 * ((t1 - t) / (t1 - t0)) + p1 * ((t - t0) / (t1 - t0))
val a2 = p1 * ((t2 - t) / (t2 - t1)) + p2 * ((t - t1) / (t2 - t1))
val a3 = p2 * ((t3 - t) / (t3 - t2)) + p3 * ((t - t2) / (t3 - t2))
val b1 = a1 * ((t2 - t) / (t2 - t0)) + a2 * ((t - t0) / (t2 - t0))
val b2 = a2 * ((t3 - t) / (t3 - t1)) + a3 * ((t - t1) / (t3 - t1))
val c = b1 * ((t2 - t) / (t2 - t1)) + b2 * ((t - t1) / (t2 - t1))
return c
}
private fun calculateT(t: Double, p0: Vector2D, p1: Vector2D): Double {
val a = (p1.x - p0.x).pow(2.0) + (p1.y - p0.y).pow(2.0)
val b = a.pow(0.5)
val c = b.pow(alpha)
return c + t
}
}
/**
* Calculates the 2D CatmullRom spline for a chain of points and returns the combined curve.
*
* For more details, see [CatmullRom2].
*
* @param points The [List] of 2D points where [CatmullRom2] is applied in groups of 4.
* @param alpha The *tension* of the curve.
* Use `0.0` for the uniform spline, `0.5` for the centripetal spline, `1.0` for the chordal spline.
* @param loop Whether to connect the first and last point, such that it forms a closed shape.
*/
class CatmullRomChain2(points: List<Vector2D>, alpha: Double = 0.5, val loop: Boolean = false) {
val segments = if (!loop) {
val startPoints = points.take(2)
val endPoints = points.takeLast(2)
val mirrorStart =
startPoints.first() - (startPoints.last() - startPoints.first()).normalized
val mirrorEnd = endPoints.last() + (endPoints.last() - endPoints.first()).normalized
(listOf(mirrorStart) + points + listOf(mirrorEnd)).windowed(4, 1).map {
CatmullRom2(it[0], it[1], it[2], it[3], alpha)
}
} else {
val cleanPoints = if (loop && points.first().distanceTo(points.last()) <= 1.0E-6) {
points.dropLast(1)
} else {
points
}
(cleanPoints + cleanPoints.take(3)).windowed(4, 1).map {
CatmullRom2(it[0], it[1], it[2], it[3], alpha)
}
}
fun positions(steps: Int = segments.size * 4): List<Vector2D> {
return (0..steps).map {
position(it.toDouble() / steps)
}
}
fun position(rt: Double): Vector2D {
val st = if (loop) rt.mod(1.0) else rt.coerceIn(0.0, 1.0)
val segmentIndex = (min(almostOne, st) * segments.size).toInt()
val t = (min(almostOne, st) * segments.size) - segmentIndex
return segments[segmentIndex].position(t)
}
}
fun List<Vector2D>.catmullRom(alpha: Double = 0.5, closed: Boolean) = CatmullRomChain2(this, alpha, closed)
/** Converts spline to a [Segment]. */
fun CatmullRom2.toSegment(): Segment2D {
val d1a2 = (p1 - p0).length.pow(2 * alpha)
val d2a2 = (p2 - p1).length.pow(2 * alpha)
val d3a2 = (p3 - p2).length.pow(2 * alpha)
val d1a = (p1 - p0).length.pow(alpha)
val d2a = (p2 - p1).length.pow(alpha)
val d3a = (p3 - p2).length.pow(alpha)
val b0 = p1
val b1 = (p2 * d1a2 - p0 * d2a2 + p1 * (2 * d1a2 + 3 * d1a * d2a + d2a2)) / (3 * d1a * (d1a + d2a))
val b2 = (p1 * d3a2 - p3 * d2a2 + p2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
val b3 = p2
return Segment2D(b0, b1, b2, b3)
}
/**
* Converts chain to a [ShapeContour].
*/
@Suppress("unused")
fun CatmullRomChain2.toContour(): ShapeContour =
ShapeContour(segments.map { it.toSegment() }, this.loop)

View File

@@ -0,0 +1,427 @@
package com.icegps.orx.color
import com.icegps.math.geometry.Vector3D
import com.icegps.math.geometry.Vector4D
import kotlinx.serialization.Serializable
import kotlin.math.pow
@Serializable
enum class Linearity(val certainty: Int) {
/**
* Represents a linear color space.
*
* LINEAR typically signifies that the values in the color space are in a linear relationship,
* meaning there is no gamma correction or transformation applied to the data.
*/
LINEAR(1),
/**
* Represents a standard RGB (sRGB) color space.
*
* SRGB typically refers to a non-linear color space with gamma correction applied,
* designed for consistent color representation across devices.
*/
SRGB(1),
;
fun leastCertain(other: Linearity): Linearity {
return if (this.certainty <= other.certainty) {
this
} else {
other
}
}
fun isEquivalent(other: Linearity): Boolean {
return this == other
}
}
/**
* Represents a color in the RGBA color space. Each component, including red, green, blue, and alpha (opacity),
* is represented as a `Double` in the range `[0.0, 1.0]`. The color can be defined in either linear or sRGB space,
* determined by the `linearity` property.
*
* This class provides a wide variety of utility functions for manipulating and converting colors, such as shading,
* opacity adjustment, and format transformations. It also includes methods for parsing colors from hexadecimal
* notation or vectors.
*
* @property r Red component of the color as a value between `0.0` and `1.0`.
* @property g Green component of the color as a value between `0.0` and `1.0`.
* @property b Blue component of the color as a value between `0.0` and `1.0`.
* @property alpha Alpha (opacity) component of the color as a value between `0.0` and `1.0`. Defaults to `1.0`.
* @property linearity Indicates whether the color is defined in linear or sRGB space. Defaults to [Linearity.LINEAR].
*/
@Serializable
@Suppress("EqualsOrHashCode") // generated equals() is ok, only hashCode() needs to be overridden
data class ColorRGBa(
val r: Double,
val g: Double,
val b: Double,
val alpha: Double = 1.0,
val linearity: Linearity = Linearity.LINEAR
) {
enum class Component {
R,
G,
B
}
companion object {
/**
* Calculates a color from hexadecimal value. For values with transparency
* use the [String] variant of this function.
*/
fun fromHex(hex: Int): ColorRGBa {
val r = hex and (0xff0000) shr 16
val g = hex and (0x00ff00) shr 8
val b = hex and (0x0000ff)
return ColorRGBa(r / 255.0, g / 255.0, b / 255.0, 1.0, Linearity.SRGB)
}
/**
* Calculates a color from hexadecimal notation, like in CSS.
*
* Supports the following formats
* * `RGB`
* * `RGBA`
* * `RRGGBB`
* * `RRGGBBAA`
*
* where every character is a valid hex digit between `0..f` (case-insensitive).
* Supports leading "#" or "0x".
*/
fun fromHex(hex: String): ColorRGBa {
val pos = when {
hex.startsWith("#") -> 1
hex.startsWith("0x") -> 2
else -> 0
}
fun fromHex1(str: String, pos: Int): Double {
return 17 * str[pos].digitToInt(16) / 255.0
}
fun fromHex2(str: String, pos: Int): Double {
return (16 * str[pos].digitToInt(16) + str[pos + 1].digitToInt(16)) / 255.0
}
return when (hex.length - pos) {
3 -> ColorRGBa(fromHex1(hex, pos), fromHex1(hex, pos + 1), fromHex1(hex, pos + 2), 1.0, Linearity.SRGB)
4 -> ColorRGBa(
fromHex1(hex, pos),
fromHex1(hex, pos + 1),
fromHex1(hex, pos + 2),
fromHex1(hex, pos + 3),
Linearity.SRGB
)
6 -> ColorRGBa(fromHex2(hex, pos), fromHex2(hex, pos + 2), fromHex2(hex, pos + 4), 1.0, Linearity.SRGB)
8 -> ColorRGBa(
fromHex2(hex, pos),
fromHex2(hex, pos + 2),
fromHex2(hex, pos + 4),
fromHex2(hex, pos + 6),
Linearity.SRGB
)
else -> throw IllegalArgumentException("Invalid hex length/format for '$hex'")
}
}
/** @suppress */
val PINK = fromHex(0xffc0cb)
/** @suppress */
val BLACK = ColorRGBa(0.0, 0.0, 0.0, 1.0, Linearity.SRGB)
/** @suppress */
val WHITE = ColorRGBa(1.0, 1.0, 1.0, 1.0, Linearity.SRGB)
/** @suppress */
val RED = ColorRGBa(1.0, 0.0, 0.0, 1.0, Linearity.SRGB)
/** @suppress */
val BLUE = ColorRGBa(0.0, 0.0, 1.0, 1.0, Linearity.SRGB)
/** @suppress */
val GREEN = ColorRGBa(0.0, 1.0, 0.0, 1.0, Linearity.SRGB)
/** @suppress */
val YELLOW = ColorRGBa(1.0, 1.0, 0.0, 1.0, Linearity.SRGB)
/** @suppress */
val CYAN = ColorRGBa(0.0, 1.0, 1.0, 1.0, Linearity.SRGB)
/** @suppress */
val MAGENTA = ColorRGBa(1.0, 0.0, 1.0, 1.0, Linearity.SRGB)
/** @suppress */
val GRAY = ColorRGBa(0.5, 0.5, 0.5, 1.0, Linearity.SRGB)
/** @suppress */
val TRANSPARENT = ColorRGBa(0.0, 0.0, 0.0, 0.0, Linearity.LINEAR)
/**
* Create a ColorRGBa object from a [Vector3]
* @param vector input vector, `[x, y, z]` is mapped to `[r, g, b]`
* @param alpha optional alpha value, default is 1.0
*/
fun fromVector(vector: Vector3D, alpha: Double = 1.0, linearity: Linearity = Linearity.LINEAR): ColorRGBa {
return ColorRGBa(vector.x, vector.y, vector.z, alpha, linearity)
}
/**
* Create a ColorRGBa object from a [Vector4]
* @param vector input vector, `[x, y, z, w]` is mapped to `[r, g, b, a]`
*/
fun fromVector(vector: Vector4D, linearity: Linearity = Linearity.LINEAR): ColorRGBa {
return ColorRGBa(vector.x, vector.y, vector.z, vector.w, linearity)
}
}
@Deprecated("Legacy alpha parameter name", ReplaceWith("alpha"))
val a = alpha
/**
* Creates a copy of color with adjusted opacity
* @param factor a scaling factor used for the opacity
* @return A [ColorRGBa] with scaled opacity
* @see shade
*/
fun opacify(factor: Double): ColorRGBa = ColorRGBa(r, g, b, alpha * factor, linearity)
/**
* Creates a copy of color with adjusted color
* @param factor a scaling factor used for the opacity
* @return A [ColorRGBa] with scaled colors
* @see opacify
*/
fun shade(factor: Double): ColorRGBa = ColorRGBa(r * factor, g * factor, b * factor, alpha, linearity)
/**
* Copy of the color with all of its fields clamped to `[0, 1]`
*/
@Deprecated("Use clip() instead", replaceWith = ReplaceWith("clip()"))
val saturated: ColorRGBa
get() = clip()
/**
* Copy of the color with all of its fields clamped to `[0, 1]`
*/
fun clip(): ColorRGBa = copy(
r = r.coerceIn(0.0..1.0),
g = g.coerceIn(0.0..1.0),
b = b.coerceIn(0.0..1.0),
alpha = alpha.coerceIn(0.0..1.0)
)
/**
* Returns a new instance of [ColorRGBa] where the red, green, and blue components
* are multiplied by the alpha value of the original color. The alpha value and linearity
* remain unchanged.
*
* This computed property is commonly used for adjusting the color intensity based
* on its transparency.
*/
val alphaMultiplied: ColorRGBa
get() = ColorRGBa(r * alpha, g * alpha, b * alpha, alpha, linearity)
/**
* The minimum value over `r`, `g`, `b`
* @see maxValue
*/
val minValue get() = r.coerceAtMost(g).coerceAtMost(b)
/**
* The maximum value over `r`, `g`, `b`
* @see minValue
*/
val maxValue get() = r.coerceAtLeast(g).coerceAtLeast(b)
/**
* calculate luminance value
* luminance value is according to <a>https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef</a>
*/
val luminance: Double
get() = when (linearity) {
Linearity.SRGB -> toLinear().luminance
else -> 0.2126 * r + 0.7152 * g + 0.0722 * b
}
/**
* Converts this color to the specified linearity.
*
* @param linearity The target linearity to which the color should be converted.
* Supported values are [Linearity.SRGB] and [Linearity.LINEAR].
* @return A [ColorRGBa] instance in the specified linearity.
*/
fun toLinearity(linearity: Linearity): ColorRGBa {
return when (linearity) {
Linearity.SRGB -> toSRGB()
Linearity.LINEAR -> toLinear()
}
}
/**
* calculate the contrast value between this color and the given color
* contrast value is accordingo to <a>// see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef</a>
*/
fun getContrastRatio(other: ColorRGBa): Double {
val l1 = luminance
val l2 = other.luminance
return if (l1 > l2) (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05)
}
fun toLinear(): ColorRGBa {
fun t(x: Double): Double {
return if (x <= 0.04045) x / 12.92 else ((x + 0.055) / (1 + 0.055)).pow(2.4)
}
return when (linearity) {
Linearity.SRGB -> ColorRGBa(t(r), t(g), t(b), alpha, Linearity.LINEAR)
else -> this
}
}
/**
* Convert to SRGB
* @see toLinear
*/
fun toSRGB(): ColorRGBa {
fun t(x: Double): Double {
return if (x <= 0.0031308) 12.92 * x else (1 + 0.055) * x.pow(1.0 / 2.4) - 0.055
}
return when (linearity) {
Linearity.LINEAR -> ColorRGBa(t(r), t(g), t(b), alpha, Linearity.SRGB)
else -> this
}
}
fun toRGBa(): ColorRGBa = this
// This is here because the default hashing of enums on the JVM is not stable.
override fun hashCode(): Int {
var result = r.hashCode()
result = 31 * result + g.hashCode()
result = 31 * result + b.hashCode()
result = 31 * result + alpha.hashCode()
// here we overcome the unstable hash by using the ordinal value
result = 31 * result + linearity.ordinal.hashCode()
return result
}
fun plus(right: ColorRGBa) = copy(
r = r + right.r,
g = g + right.g,
b = b + right.b,
alpha = alpha + right.alpha
)
fun minus(right: ColorRGBa) = copy(
r = r - right.r,
g = g - right.g,
b = b - right.b,
alpha = alpha - right.alpha
)
fun times(scale: Double) = copy(r = r * scale, g = g * scale, b = b * scale, alpha = alpha * scale)
fun mix(other: ColorRGBa, factor: Double): ColorRGBa {
return mix(this, other, factor)
}
fun toVector4(): Vector4D = Vector4D(r, g, b, alpha)
/**
* Retrieves the color's RGBA component value based on the specified index:
* [index] should be 0 for red, 1 for green, 2 for blue, 3 for alpha.
* Other index values throw an [IndexOutOfBoundsException].
*/
operator fun get(index: Int) = when (index) {
0 -> r
1 -> g
2 -> b
3 -> alpha
else -> throw IllegalArgumentException("unsupported index")
}
}
/**
* Weighted mix between two colors in the generic RGB color space.
* @param x the weighting of colors, a value 0.0 is equivalent to [left],
* 1.0 is equivalent to [right] and at 0.5 both colors contribute to the result equally
* @return a mix of [left] and [right] weighted by [x]
*/
fun mix(left: ColorRGBa, right: ColorRGBa, x: Double): ColorRGBa {
val sx = x.coerceIn(0.0, 1.0)
if (left.linearity.isEquivalent(right.linearity)) {
return ColorRGBa(
(1.0 - sx) * left.r + sx * right.r,
(1.0 - sx) * left.g + sx * right.g,
(1.0 - sx) * left.b + sx * right.b,
(1.0 - sx) * left.alpha + sx * right.alpha,
linearity = left.linearity.leastCertain(right.linearity)
)
} else {
return when (right.linearity) {
Linearity.LINEAR -> {
mix(left.toLinear(), right.toLinear(), x)
}
Linearity.SRGB -> {
mix(left.toSRGB(), right.toSRGB(), x)
}
}
}
}
/**
* Shorthand for calling [ColorRGBa].
* Specify only one value to obtain a shade of gray.
* @param r red in `[0,1]`
* @param g green in `[0,1]`
* @param b blue in `[0,1]`
* @param a alpha in `[0,1]`, defaults to `1.0`
*/
fun rgb(r: Double, g: Double, b: Double, a: Double = 1.0) = ColorRGBa(r, g, b, a, linearity = Linearity.LINEAR)
/**
* Shorthand for calling [ColorRGBa].
* @param gray shade of gray in `[0,1]`
* @param a alpha in `[0,1]`, defaults to `1.0`
*/
fun rgb(gray: Double, a: Double = 1.0) = ColorRGBa(gray, gray, gray, a, linearity = Linearity.LINEAR)
/**
* Create a color in RGBa space
* This function is a shorthand for using the ColorRGBa constructor
* @param r red in `[0,1]`
* @param g green in `[0,1]`
* @param b blue in `[0,1]`
* @param a alpha in `[0,1]`
*/
@Deprecated("Use rgb(r, g, b, a)", ReplaceWith("rgb(r, g, b, a)"), DeprecationLevel.WARNING)
fun rgba(r: Double, g: Double, b: Double, a: Double) = ColorRGBa(r, g, b, a, linearity = Linearity.LINEAR)
/**
* Shorthand for calling [ColorRGBa.fromHex].
* Creates a [ColorRGBa] with [Linearity.SRGB] from a hex string.
* @param hex string encoded hex value, for example `"ffc0cd"`
*/
fun rgb(hex: String) = ColorRGBa.fromHex(hex)
/**
* Converts RGB integer color values into a ColorRGBa object with sRGB linearity.
*
* @param red The red component of the color, in the range 0-255.
* @param green The green component of the color, in the range 0-255.
* @param blue The blue component of the color, in the range 0-255.
* @param alpha The alpha (transparency) component of the color, in the range 0-255. Default value is 255 (fully opaque).
*/
fun rgb(red: Int, green: Int, blue: Int, alpha: Int = 255) =
ColorRGBa(red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0, Linearity.SRGB)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
package com.icegps.orx.ktx
import org.openrndr.color.ColorRGBa
import com.icegps.orx.color.ColorRGBa
/**
* @author tabidachinokaze

View File

@@ -1,22 +0,0 @@
package com.icegps.orx.ktx
import com.icegps.common.helper.GeoHelper
import com.mapbox.geojson.Point
import org.openrndr.math.Vector2
fun Vector2.niceStr(): String {
return "[$x, $y, 0.0]".format(this)
}
fun List<Vector2>.niceStr(): String {
return joinToString(", ", "[", "]") {
it.niceStr()
}
}
fun Vector2.toMapboxPoint(): Point {
val geoHelper = GeoHelper.getSharedInstance()
return geoHelper.enuToWGS84Object(GeoHelper.ENU(x, y)).run {
Point.fromLngLat(lon, lat, hgt)
}
}

View File

@@ -12,4 +12,13 @@ fun Vector2D.toMapboxPoint(): Point {
val geoHelper = GeoHelper.getSharedInstance()
val wgs84 = geoHelper.enuToWGS84Object(GeoHelper.ENU(x = x, y = y))
return Point.fromLngLat(wgs84.lon, wgs84.lat)
}
}
/**
* Interpolates between the current vector and the given vector `o` by the specified mixing factor.
*
* @param o The target vector to interpolate towards.
* @param mix A mixing factor between 0 and 1 where `0` results in the current vector and `1` results in the vector `o`.
* @return A new vector that is the result of the interpolation.
*/
fun Vector2D.mix(o: Vector2D, mix: Double): Vector2D = this * (1 - mix) + o * mix

View File

@@ -1,22 +0,0 @@
package com.icegps.orx.ktx
import org.openrndr.math.Vector3
fun Vector3.niceStr(): String {
return "[$x, $y, $z]".format(this)
}
fun List<Vector3>.niceStr(): String {
return joinToString(", ", "[", "]") {
it.niceStr()
}
}
val List<Vector3>.area: org.openrndr.shape.Rectangle
get() {
val minX = minOf { it.x }
val maxX = maxOf { it.x }
val minY = minOf { it.y }
val maxY = maxOf { it.y }
return org.openrndr.shape.Rectangle(x = minX, y = minY, width = maxX - minX, height = maxY - minY)
}

View File

@@ -0,0 +1,219 @@
package com.icegps.orx.marchingsquares
import com.icegps.math.geometry.Rectangle
import com.icegps.math.geometry.Vector2D
import com.icegps.math.geometry.Vector2I
import com.icegps.orx.ktx.mix
import kotlin.math.max
import kotlin.math.min
private const val closeEpsilon = 1E-6
data class Segment2D(
val start: Vector2D,
val control: List<Vector2D>,
val end: Vector2D,
val corner: Boolean = false
)
fun Segment2D(start: Vector2D, end: Vector2D, corner: Boolean = true) =
Segment2D(start, emptyList(), end, corner)
fun Segment2D(start: Vector2D, c0: Vector2D, c1: Vector2D, end: Vector2D, corner: Boolean = true) =
Segment2D(start, listOf(c0, c1), end, corner)
data class ShapeContour(
val segments: List<Segment2D>,
val closed: Boolean,
) {
companion object {
val EMPTY = ShapeContour(
segments = emptyList(),
closed = false,
)
/**
* Creates a ShapeContour from a list of points, specifying whether the contour is closed and its y-axis polarity.
*
* @param points A list of points (Vector2) defining the vertices of the contour.
* @param closed Boolean indicating whether the contour should be closed (forms a loop).
* @return A ShapeContour object representing the resulting contour.
*/
fun fromPoints(
points: List<Vector2D>,
closed: Boolean,
): ShapeContour = if (points.isEmpty()) {
EMPTY
} else {
if (!closed) {
ShapeContour((0 until points.size - 1).map {
Segment2D(
points[it],
points[it + 1]
)
}, false)
} else {
val d = (points.last() - points.first()).lengthSquared
val usePoints = if (d > closeEpsilon) points else points.dropLast(1)
ShapeContour((usePoints.indices).map {
Segment2D(
usePoints[it],
usePoints[(it + 1) % usePoints.size]
)
}, true)
}
}
}
}
data class LineSegment(val start: Vector2D, val end: Vector2D)
/**
* Find contours for a function [f] using the marching squares algorithm. A contour is found when f(x) crosses zero.
* @param f the function
* @param area a rectangular area in which the function should be evaluated
* @param cellSize the size of the cells, smaller size gives higher resolution
* @param useInterpolation intersection points will be interpolated if true, default true
* @return a list of [ShapeContour] instances
*/
fun findContours(
f: (Vector2D) -> Double,
area: Rectangle,
cellSize: Double,
useInterpolation: Boolean = true
): List<ShapeContour> {
val segments = mutableListOf<LineSegment>()
val values = mutableMapOf<Vector2I, Double>()
val segmentsMap = mutableMapOf<Vector2D, MutableList<LineSegment>>()
for (y in 0 until (area.height / cellSize).toInt()) {
for (x in 0 until (area.width / cellSize).toInt()) {
values[Vector2I(x, y)] = f(Vector2D(x * cellSize + area.x, y * cellSize + area.y))
}
}
val zero = 0.0
for (y in 0 until (area.height / cellSize).toInt()) {
for (x in 0 until (area.width / cellSize).toInt()) {
// Here we check if we are at a right or top border. This is to ensure we create closed contours
// later on in the process.
val v00 = if (x == 0 || y == 0) zero else (values[Vector2I(x, y)] ?: zero)
val v10 = if (y == 0) zero else (values[Vector2I(x + 1, y)] ?: zero)
val v01 = if (x == 0) zero else (values[Vector2I(x, y + 1)] ?: zero)
val v11 = (values[Vector2I(x + 1, y + 1)] ?: zero)
val p00 = Vector2D(x.toDouble(), y.toDouble()) * cellSize + area.topLeft
val p10 = Vector2D((x + 1).toDouble(), y.toDouble()) * cellSize + area.topLeft
val p01 = Vector2D(x.toDouble(), (y + 1).toDouble()) * cellSize + area.topLeft
val p11 = Vector2D((x + 1).toDouble(), (y + 1).toDouble()) * cellSize + area.topLeft
val index = (if (v00 >= 0.0) 1 else 0) +
(if (v10 >= 0.0) 2 else 0) +
(if (v01 >= 0.0) 4 else 0) +
(if (v11 >= 0.0) 8 else 0)
fun blend(v1: Double, v2: Double): Double {
if (useInterpolation) {
require(!v1.isNaN() && !v2.isNaN()) {
"Input values v1=$v1 or v2=$v2 are NaN, which is not allowed."
}
val f1 = min(v1, v2)
val f2 = max(v1, v2)
val v = (-f1) / (f2 - f1)
require(v == v && v in 0.0..1.0) {
"Invalid value calculated during interpolation: v=$v"
}
return if (f1 == v1) {
v
} else {
1.0 - v
}
} else {
return 0.5
}
}
fun emitLine(
p00: Vector2D, p01: Vector2D, v00: Double, v01: Double,
p10: Vector2D, p11: Vector2D, v10: Double, v11: Double
) {
val r0 = blend(v00, v01)
val r1 = blend(v10, v11)
val v0 = p00.mix(p01, r0)
val v1 = p10.mix(p11, r1)
val l0 = LineSegment(v0, v1)
segmentsMap.getOrPut(v1) { mutableListOf() }.add(l0)
segmentsMap.getOrPut(v0) { mutableListOf() }.add(l0)
segments.add(l0)
}
when (index) {
0, 15 -> {}
1, 15 xor 1 -> {
emitLine(p00, p01, v00, v01, p00, p10, v00, v10)
}
2, 15 xor 2 -> {
emitLine(p00, p10, v00, v10, p10, p11, v10, v11)
}
3, 15 xor 3 -> {
emitLine(p00, p01, v00, v01, p10, p11, v10, v11)
}
4, 15 xor 4 -> {
emitLine(p00, p01, v00, v01, p01, p11, v01, v11)
}
5, 15 xor 5 -> {
emitLine(p00, p10, v00, v10, p01, p11, v01, v11)
}
6, 15 xor 6 -> {
emitLine(p00, p01, v00, v01, p00, p10, v00, v10)
emitLine(p01, p11, v01, v11, p10, p11, v10, v11)
}
7, 15 xor 7 -> {
emitLine(p01, p11, v01, v11, p10, p11, v10, v11)
}
}
}
}
val processedSegments = mutableSetOf<LineSegment>()
val contours = mutableListOf<ShapeContour>()
for (segment in segments) {
if (segment in processedSegments) {
continue
} else {
val collected = mutableListOf<Vector2D>()
var current: LineSegment? = segment
var closed = true
var lastVertex = Vector2D.INFINITY
do {
current!!
if (lastVertex.squaredDistanceTo(current.start) > 1E-5) {
collected.add(current.start)
}
lastVertex = current.start
processedSegments.add(current)
if (segmentsMap[current.start]!!.size < 2) {
closed = false
}
val hold = current
current = segmentsMap[current.start]?.firstOrNull { it !in processedSegments }
if (current == null) {
current = segmentsMap[hold.end]?.firstOrNull { it !in processedSegments }
}
} while (current != segment && current != null)
contours.add(ShapeContour.fromPoints(collected, closed = closed))
}
}
return contours
}

View File

@@ -1,91 +0,0 @@
package com.icegps.orx.triangulation
import org.openrndr.extra.triangulation.Delaunay
import org.openrndr.math.Vector3
import org.openrndr.shape.path3D
/**
* Kotlin/OPENRNDR idiomatic interface to `Delaunay`
*/
class DelaunayTriangulation3D(val points: List<Vector3>) {
val delaunay: Delaunay = Delaunay.from(points.map { it.xy })
fun neighbors(pointIndex: Int): Sequence<Int> {
return delaunay.neighbors(pointIndex)
}
fun neighborPoints(pointIndex: Int): List<Vector3> {
return neighbors(pointIndex).map { points[it] }.toList()
}
fun triangleIndices(): List<IntArray> {
val list = mutableListOf<IntArray>()
for (i in delaunay.triangles.indices step 3) {
list.add(
intArrayOf(
delaunay.triangles[i],
delaunay.triangles[i + 1],
delaunay.triangles[i + 2]
)
)
}
return list
}
fun triangles(filterPredicate: (Int, Int, Int) -> Boolean = { _, _, _ -> true }): MutableList<Triangle3D> {
val list = mutableListOf<Triangle3D>()
for (i in delaunay.triangles.indices step 3) {
val t0 = delaunay.triangles[i]
val t1 = delaunay.triangles[i + 1]
val t2 = delaunay.triangles[i + 2]
// originally they are defined *counterclockwise*
if (filterPredicate(t2, t1, t0)) {
val p1 = points[t0]
val p2 = points[t1]
val p3 = points[t2]
list.add(Triangle3D(p1, p2, p3))
}
}
return list
}
// Inner edges of the delaunay triangulation (without hull)
fun halfedges() = path3D {
for (i in delaunay.halfedges.indices) {
val j = delaunay.halfedges[i]
if (j < i) continue
val ti = delaunay.triangles[i]
val tj = delaunay.triangles[j]
moveTo(points[ti])
lineTo(points[tj])
}
}
fun hull() = path3D {
for (h in delaunay.hull) {
moveOrLineTo(points[h])
}
close()
}
fun nearest(query: Vector3): Int = delaunay.find(query.x, query.y)
fun nearestPoint(query: Vector3): Vector3 = points[nearest(query)]
}
/**
* Computes the Delaunay triangulation for the list of 2D points.
*
* The Delaunay triangulation is a triangulation of a set of points such that
* no point is inside the circumcircle of any triangle. It maximizes the minimum
* angle of all the angles in the triangles, avoiding skinny triangles.
*
* @return A DelaunayTriangulation object representing the triangulation of the given points.
*/
fun List<Vector3>.delaunayTriangulation(): DelaunayTriangulation3D {
return DelaunayTriangulation3D(this)
}

View File

@@ -1,24 +0,0 @@
package com.icegps.orx.triangulation
import org.openrndr.math.Vector3
import org.openrndr.shape.BezierSegment
import org.openrndr.shape.Path
import org.openrndr.shape.Path3D
/**
* @author tabidachinokaze
* @date 2025/11/24
*/
data class Triangle3D(
val x1: Vector3,
val x2: Vector3,
val x3: Vector3,
) : Path<Vector3> {
val path = Path3D.fromPoints(points = listOf(x1, x2, x3), closed = true)
override fun sub(t0: Double, t1: Double): Path<Vector3> = path.sub(t0, t1)
override val closed: Boolean get() = path.closed
override val empty: Boolean get() = path.empty
override val infinity: Vector3 get() = path.infinity
override val segments: List<BezierSegment<Vector3>> get() = path.segments
}

View File

@@ -3,11 +3,14 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
dependencies {
implementation(project(":math"))
}

View File

@@ -1,4 +1,4 @@
package org.openrndr.extra.triangulation
package com.icegps.triangulation
import kotlin.math.*

View File

@@ -1,8 +1,7 @@
package org.openrndr.extra.triangulation
package com.icegps.triangulation
import org.openrndr.extra.triangulation.Delaunay.Companion.from
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import com.icegps.math.geometry.Vector2D
import com.icegps.triangulation.Delaunay.Companion.from
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
@@ -42,7 +41,7 @@ class Delaunay(val points: DoubleArray) {
*
* @property points a list of 2D points
*/
fun from(points: List<Vector2>): Delaunay {
fun from(points: List<Vector2D>): Delaunay {
val n = points.size
val coords = DoubleArray(n * 2)
@@ -74,13 +73,13 @@ class Delaunay(val points: DoubleArray) {
init()
}
fun neighbors(i:Int) = sequence<Int> {
fun neighbors(i: Int) = sequence<Int> {
val e0 = inedges[i]
if (e0 != -1) {
var e = e0
var p0 = -1
loop@do {
loop@ do {
p0 = triangles[e]
yield(p0)
e = if (e % 3 == 2) e - 2 else e + 1
@@ -109,26 +108,28 @@ class Delaunay(val points: DoubleArray) {
for (i in 0 until triangles.size step 3) {
val a = 2 * triangles[i]
val b = 2 * triangles[i + 1]
val c = 2 * triangles[i + 2]
val c = 2 * triangles[i + 2]
val coords = points
val cross = (coords[c] - coords[a]) * (coords[b + 1] - coords[a + 1])
- (coords[b] - coords[a]) * (coords[c + 1] - coords[a + 1])
-(coords[b] - coords[a]) * (coords[c + 1] - coords[a + 1])
if (cross > 1e-10) return false;
}
return true
}
private fun jitter(x:Double, y:Double, r:Double): DoubleArray {
return doubleArrayOf(x + sin(x+y) * r, y + cos(x-y)*r)
private fun jitter(x: Double, y: Double, r: Double): DoubleArray {
return doubleArrayOf(x + sin(x + y) * r, y + cos(x - y) * r)
}
fun init() {
if (hull.size > 2 && collinear()) {
println("warning: triangulation is collinear")
val r = 1E-8
for (i in 0 until points.size step 2) {
val p = jitter(points[i], points[i+1], r)
val p = jitter(points[i], points[i + 1], r)
points[i] = p[0]
points[i+1] = p[1]
points[i + 1] = p[1]
}
delaunator = Delaunator(points)
@@ -221,12 +222,4 @@ class Delaunay(val points: DoubleArray) {
return c
}
/**
* Generates a Voronoi diagram based on the current Delaunay triangulation and the provided bounds.
*
* @param bounds A rectangle defining the boundaries within which the Voronoi diagram will be generated.
* @return A Voronoi instance representing the resulting Voronoi diagram.
*/
fun voronoi(bounds: Rectangle): Voronoi = Voronoi(this, bounds)
}

View File

@@ -1,24 +1,19 @@
package org.openrndr.extra.triangulation
package com.icegps.triangulation
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import org.openrndr.shape.Triangle
import org.openrndr.shape.contour
import org.openrndr.shape.contours
import com.icegps.math.geometry.Vector3D
import com.icegps.math.geometry.toVector2D
/**
* Kotlin/OPENRNDR idiomatic interface to `Delaunay`
*/
class DelaunayTriangulation(val points: List<Vector2>) {
val delaunay: Delaunay = Delaunay.from(points)
fun voronoiDiagram(bounds: Rectangle) = VoronoiDiagram(this, bounds)
class DelaunayTriangulation(val points: List<Vector3D>) {
val delaunay: Delaunay = Delaunay.from(points.map { it.toVector2D() })
fun neighbors(pointIndex: Int): Sequence<Int> {
return delaunay.neighbors(pointIndex)
}
fun neighborPoints(pointIndex: Int): List<Vector2> {
fun neighborPoints(pointIndex: Int): List<Vector3D> {
return neighbors(pointIndex).map { points[it] }.toList()
}
@@ -55,30 +50,9 @@ class DelaunayTriangulation(val points: List<Vector2>) {
return list
}
// Inner edges of the delaunay triangulation (without hull)
fun halfedges() = contours {
for (i in delaunay.halfedges.indices) {
val j = delaunay.halfedges[i]
fun nearest(query: Vector3D): Int = delaunay.find(query.x, query.y)
if (j < i) continue
val ti = delaunay.triangles[i]
val tj = delaunay.triangles[j]
moveTo(points[ti])
lineTo(points[tj])
}
}
fun hull() = contour {
for (h in delaunay.hull) {
moveOrLineTo(points[h])
}
close()
}
fun nearest(query: Vector2): Int = delaunay.find(query.x, query.y)
fun nearestPoint(query: Vector2): Vector2 = points[nearest(query)]
fun nearestPoint(query: Vector3D): Vector3D = points[nearest(query)]
}
/**
@@ -90,6 +64,6 @@ class DelaunayTriangulation(val points: List<Vector2>) {
*
* @return A DelaunayTriangulation object representing the triangulation of the given points.
*/
fun List<Vector2>.delaunayTriangulation(): DelaunayTriangulation {
fun List<Vector3D>.delaunayTriangulation(): DelaunayTriangulation {
return DelaunayTriangulation(this)
}

View File

@@ -1,4 +1,4 @@
package org.openrndr.extra.triangulation
package com.icegps.triangulation
import kotlin.math.pow

View File

@@ -1,4 +1,4 @@
package org.openrndr.extra.triangulation
package com.icegps.triangulation
fun orient2d(bx: Double, by: Double, ax: Double, ay: Double, cx: Double, cy: Double): Double {
// (ax,ay) (bx,by) are swapped such that the sign of the determinant is flipped. which is what Delaunator.kt expects.

View File

@@ -0,0 +1,13 @@
package com.icegps.triangulation
import com.icegps.math.geometry.Vector3D
/**
* @author tabidachinokaze
* @date 2025/11/26
*/
data class Triangle(
val x1: Vector3D,
val x2: Vector3D,
val x3: Vector3D,
)

View File

@@ -1,622 +0,0 @@
package org.openrndr.extra.triangulation
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.sign
/*
ISC License
Copyright 2021 Ricardo Matias.
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
*/
/**
* This is a fast library for computing the Voronoi diagram of a set of two-dimensional points.
* The Voronoi diagram is constructed by connecting the circumcenters of adjacent triangles
* in the Delaunay triangulation.
*
* @description Port of d3-delaunay (JavaScript) library - https://github.com/d3/d3-delaunay
* @property points flat positions' array - [x0, y0, x1, y1..]
*
* @since 9258fa3 - commit
* @author Ricardo Matias
*/
class Voronoi(val delaunay: Delaunay, val bounds: Rectangle) {
private val _circumcenters = DoubleArray(delaunay.points.size * 2)
lateinit var circumcenters: DoubleArray
private set
val vectors = DoubleArray(delaunay.points.size * 2)
init {
init()
}
fun update() {
delaunay.update()
init()
}
fun init() {
val points = delaunay.points
if (delaunay.points.isEmpty()) {
return
}
val triangles = delaunay.triangles
val hull = delaunay.hull
if (points.size == 2) {
_circumcenters[0] = points[0]
_circumcenters[1] = points[1]
circumcenters = _circumcenters
return
}
circumcenters = _circumcenters.copyOf(delaunay.triangles.size / 3 * 2)
// Compute circumcenters
var i = 0
var j = 0
var x: Double
var y: Double
while (i < triangles.size) {
val t1 = triangles[i] * 2
val t2 = triangles[i + 1] * 2
val t3 = triangles[i + 2] * 2
val x1 = points[t1]
val y1 = points[t1 + 1]
val x2 = points[t2]
val y2 = points[t2 + 1]
val x3 = points[t3]
val y3 = points[t3 + 1]
val dx = x2 - x1
val dy = y2 - y1
val ex = x3 - x1
val ey = y3 - y1
val ab = (dx * ey - dy * ex) * 2
if (abs(ab) < 1e-9) {
var a = 1e9
val r = triangles[0] * 2
a *= sign((points[r] - x1) * ey - (points[r + 1] - y1) * ex)
x = (x1 + x3) / 2 - a * ey
y = (y1 + y3) / 2 + a * ex
} else {
val d = 1 / ab
val bl = dx * dx + dy * dy
val cl = ex * ex + ey * ey
x = x1 + (ey * bl - dy * cl) * d
y = y1 + (dx * cl - ex * bl) * d
}
circumcenters[j] = x
circumcenters[j + 1] = y
i += 3
j += 2
}
// Compute exterior cell rays.
var h = hull[hull.size - 1]
var p0: Int
var p1 = h * 4
var x0: Double
var x1 = points[2 * h]
var y0: Double
var y1 = points[2 * h + 1]
var y01: Double
var x10: Double
vectors.fill(0.0)
for (idx in hull.indices) {
h = hull[idx]
p0 = p1
x0 = x1
y0 = y1
p1 = h * 4
x1 = points[2 * h]
y1 = points[2 * h + 1]
y01 = y0 - y1
x10 = x1 - x0
vectors[p0 + 2] = y01
vectors[p1] = y01
vectors[p0 + 3] = x10
vectors[p1 + 1] = x10
}
}
private fun cell(i: Int): MutableList<Double>? {
val inedges = delaunay.inedges
val halfedges = delaunay.halfedges
val triangles = delaunay.triangles
val e0 = inedges[i]
if (e0 == -1) return null // coincident point
val points = mutableListOf<Double>()
var e = e0
do {
val t = floor(e / 3.0).toInt()
points.add(circumcenters[t * 2])
points.add(circumcenters[t * 2 + 1])
e = if (e % 3 == 2) e - 2 else e + 1 // next half edge
if (triangles[e] != i) break
e = halfedges[e]
} while (e != e0 && e != -1)
return points
}
fun neighbors(i: Int) = sequence {
val ci = clip(i)
if (ci != null) {
for (j in delaunay.neighbors(i)) {
val cj = clip(j)
if (cj != null) {
val li = ci.size
val lj = cj.size
loop@ for (ai in 0 until ci.size step 2) {
for (aj in 0 until cj.size step 2) {
if (ci[ai] == cj[aj]
&& ci[ai + 1] == cj[aj + 1]
&& ci[(ai + 2) % li] == cj[(aj + lj - 2) % lj]
&& ci[(ai + 3) % li] == cj[(aj + lj - 1) % lj]
) {
yield(j)
break@loop
}
}
}
}
}
}
}
internal fun clip(i: Int): List<Double>? {
// degenerate case (1 valid point: return the box)
if (i == 0 && delaunay.points.size == 2) {
return listOf(
bounds.xmax,
bounds.ymin,
bounds.xmax,
bounds.ymax,
bounds.xmin,
bounds.ymax,
bounds.xmin,
bounds.ymin
)
}
val points = cell(i) ?: return null
val clipVectors = vectors
val v = i * 4
val a = !clipVectors[v].isFalsy()
val b = !clipVectors[v + 1].isFalsy()
return if (a || b) {
this.clipInfinite(i, points, clipVectors[v], clipVectors[v + 1], clipVectors[v + 2], clipVectors[v + 3])
} else {
this.clipFinite(i, points)
}
}
private fun clipInfinite(
i: Int,
points: MutableList<Double>,
vx0: Double,
vy0: Double,
vxn: Double,
vyn: Double
): List<Double>? {
var P: MutableList<Double>? = points.mutableCopyOf()
P!!
project(P[0], P[1], vx0, vy0)?.let { p -> P!!.add(0, p[1]); P!!.add(0, p[0]) }
project(P[P.size - 2], P[P.size - 1], vxn, vyn)?.let { p -> P!!.add(p[0]); P!!.add(p[1]) }
P = this.clipFinite(i, P!!)
var n = 0
if (P != null) {
n = P!!.size
var c0 = -1
var c1 = edgeCode(P[n - 2], P[n - 1])
var j = 0
var n = P.size
while (j < n) {
c0 = c1
c1 = edgeCode(P[j], P[j + 1])
if (c0 != 0 && c1 != 0) {
j = edge(i, c0, c1, P, j)
n = P.size
}
j += 2
}
} else if (this.contains(i, (bounds.xmin + bounds.xmax) / 2.0, (bounds.ymin + bounds.ymax) / 2.0)) {
P = mutableListOf(
bounds.xmin,
bounds.ymin,
bounds.xmax,
bounds.ymin,
bounds.xmax,
bounds.ymax,
bounds.xmin,
bounds.ymax
)
}
return P
}
private fun clipFinite(i: Int, points: MutableList<Double>): MutableList<Double>? {
val n = points.size
val P = mutableListOf<Double>()
var x0: Double
var y0: Double
var x1 = points[n - 2]
var y1 = points[n - 1]
var c0: Int
var c1: Int = regionCode(x1, y1)
var e0: Int? = null
var e1: Int? = 0
for (j in 0 until n step 2) {
x0 = x1
y0 = y1
x1 = points[j]
y1 = points[j + 1]
c0 = c1
c1 = regionCode(x1, y1)
if (c0 == 0 && c1 == 0) {
e0 = e1
e1 = 0
P.add(x1)
P.add(y1)
} else {
var S: DoubleArray?
var sx0: Double
var sy0: Double
var sx1: Double
var sy1: Double
if (c0 == 0) {
S = clipSegment(x0, y0, x1, y1, c0, c1)
if (S == null) continue
sx0 = S[0]
sy0 = S[1]
sx1 = S[2]
sy1 = S[3]
} else {
S = clipSegment(x1, y1, x0, y0, c1, c0)
if (S == null) continue
sx1 = S[0]
sy1 = S[1]
sx0 = S[2]
sy0 = S[3]
e0 = e1
e1 = this.edgeCode(sx0, sy0)
if (e0 != 0 && e1 != 0) this.edge(i, e0!!, e1, P, P.size)
P.add(sx0)
P.add(sy0)
}
e0 = e1
e1 = this.edgeCode(sx1, sy1);
if (e0.isTruthy() && e1.isTruthy()) this.edge(i, e0!!, e1, P, P.size);
P.add(sx1)
P.add(sy1)
}
}
if (P.isNotEmpty()) {
e0 = e1
e1 = this.edgeCode(P[0], P[1])
if (e0.isTruthy() && e1.isTruthy()) this.edge(i, e0!!, e1!!, P, P.size);
} else if (this.contains(i, (bounds.xmin + bounds.xmax) / 2, (bounds.ymin + bounds.ymax) / 2)) {
return mutableListOf(
bounds.xmax,
bounds.ymin,
bounds.xmax,
bounds.ymax,
bounds.xmin,
bounds.ymax,
bounds.xmin,
bounds.ymin
)
} else {
return null
}
return P
}
private fun clipSegment(x0: Double, y0: Double, x1: Double, y1: Double, c0: Int, c1: Int): DoubleArray? {
var nx0: Double = x0
var ny0: Double = y0
var nx1: Double = x1
var ny1: Double = y1
var nc0: Int = c0
var nc1: Int = c1
while (true) {
if (nc0 == 0 && nc1 == 0) return doubleArrayOf(nx0, ny0, nx1, ny1)
// SHAKY STUFF
if ((nc0 and nc1) != 0) return null
var x: Double
var y: Double
val c: Int = if (nc0 != 0) nc0 else nc1
when {
(c and 0b1000) != 0 -> {
x = nx0 + (nx1 - nx0) * (bounds.ymax - ny0) / (ny1 - ny0)
y = bounds.ymax;
}
(c and 0b0100) != 0 -> {
x = nx0 + (nx1 - nx0) * (bounds.ymin - ny0) / (ny1 - ny0)
y = bounds.ymin
}
(c and 0b0010) != 0 -> {
y = ny0 + (ny1 - ny0) * (bounds.xmax - nx0) / (nx1 - nx0)
x = bounds.xmax
}
else -> {
y = ny0 + (ny1 - ny0) * (bounds.xmin - nx0) / (nx1 - nx0)
x = bounds.xmin;
}
}
if (nc0 != 0) {
nx0 = x
ny0 = y
nc0 = this.regionCode(nx0, ny0)
} else {
nx1 = x
ny1 = y
nc1 = this.regionCode(nx1, ny1)
}
}
}
private fun regionCode(x: Double, y: Double): Int {
val xcode = when {
x < bounds.xmin -> 0b0001
x > bounds.xmax -> 0b0010
else -> 0b0000
}
val ycode = when {
y < bounds.ymin -> 0b0100
y > bounds.ymax -> 0b1000
else -> 0b0000
}
return xcode or ycode
}
private fun contains(i: Int, x: Double, y: Double): Boolean {
if (x.isNaN() || y.isNaN()) return false
return this.delaunay.step(i, x, y) == i;
}
private fun edge(i: Int, e0: Int, e1: Int, p: MutableList<Double>, j: Int): Int {
var j = j
var e = e0
loop@ while (e != e1) {
var x: Double = Double.NaN
var y: Double = Double.NaN
when (e) {
0b0101 -> { // top-left
e = 0b0100
continue@loop
}
0b0100 -> { // top
e = 0b0110
x = bounds.xmax
y = bounds.ymin
}
0b0110 -> { // top-right
e = 0b0010
continue@loop
}
0b0010 -> { // right
e = 0b1010
x = bounds.xmax
y = bounds.ymax
}
0b1010 -> { // bottom-right
e = 0b1000
continue@loop
}
0b1000 -> { // bottom
e = 0b1001
x = bounds.xmin
y = bounds.ymax
}
0b1001 -> { // bottom-left
e = 0b0001
continue@loop
}
0b0001 -> { // left
e = 0b0101
x = bounds.xmin
y = bounds.ymin
}
}
if (((j < p.size && (p[j] != x)) || ((j + 1) < p.size && p[j + 1] != y)) && contains(i, x, y)) {
require(!x.isNaN())
require(!y.isNaN())
p.add(j, y)
p.add(j, x)
j += 2
} else if (j >= p.size && contains(i, x, y)) {
require(!x.isNaN())
require(!y.isNaN())
p.add(x)
p.add(y)
j += 2
}
}
if (p.size > 4) {
var idx = 0
var n = p.size
while (idx < n) {
val j = (idx + 2) % p.size
val k = (idx + 4) % p.size
if ((p[idx] == p[j] && p[j] == p[k])
|| (p[idx + 1] == p[j + 1] && p[j + 1] == p[k + 1])
) {
// SHAKY
p.removeAt(j)
p.removeAt(j)
idx -= 2
n -= 2
}
idx += 2
}
}
return j
}
private fun project(x0: Double, y0: Double, vx: Double, vy: Double): Vector2? {
var t = Double.POSITIVE_INFINITY
var c: Double
var x = Double.NaN
var y = Double.NaN
// top
if (vy < 0) {
if (y0 <= bounds.ymin) return null
c = (bounds.ymin - y0) / vy
if (c < t) {
t = c
y = bounds.ymin
x = x0 + t * vx
}
} else if (vy > 0) { // bottom
if (y0 >= bounds.ymax) return null
c = (bounds.ymax - y0) / vy
if (c < t) {
t = c
y = bounds.ymax
x = x0 + t * vx
}
}
// right
if (vx > 0) {
if (x0 >= bounds.xmax) return null
c = (bounds.xmax - x0) / vx
if (c < t) {
t = c
x = bounds.xmax
y = y0 + t * vy
}
} else if (vx < 0) { // left
if (x0 <= bounds.xmin) return null
c = (bounds.xmin - x0) / vx
if (c < t) {
t = c
x = bounds.xmin
y = y0 + t * vy
}
}
if (x.isNaN() || y.isNaN()) return null
return Vector2(x, y)
}
private fun edgeCode(x: Double, y: Double): Int {
val xcode = when (x) {
bounds.xmin -> 0b0001
bounds.xmax -> 0b0010
else -> 0b0000
}
val ycode = when (y) {
bounds.ymin -> 0b0100
bounds.ymax -> 0b1000
else -> 0b0000
}
return xcode or ycode
}
}
private fun Int?.isTruthy(): Boolean {
return (this != null && this != 0)
}
private fun <T> List<T>.mutableCopyOf(): MutableList<T> {
val original = this
return mutableListOf<T>().apply { addAll(original) }
}
private val Rectangle.xmin: Double
get() = this.corner.x
private val Rectangle.xmax: Double
get() = this.corner.x + width
private val Rectangle.ymin: Double
get() = this.corner.y
private val Rectangle.ymax: Double
get() = this.corner.y + height
private fun Double?.isFalsy() = this == null || this == -0.0 || this == 0.0 || isNaN()

View File

@@ -1,7 +0,0 @@
package com.icegps.triangulation
/**
* @author tabidachinokaze
* @date 2025/11/24
*/
typealias Vector2 = Vector

View File

@@ -121,6 +121,13 @@ data class Vector2D(val x: Double, val y: Double) : IsAlmostEquals<Vector2D> {
fun distanceTo(x: Int, y: Int): Double = this.distanceTo(x.toDouble(), y.toDouble())
fun distanceTo(that: Vector2D): Double = distanceTo(that.x, that.y)
/** Calculates the squared Euclidean distance to [other]. */
fun squaredDistanceTo(other: Vector2D): Double {
val dx = other.x - x
val dy = other.y - y
return dx * dx + dy * dy
}
infix fun cross(that: Vector2D): Double = crossProduct(this, that)
infix fun dot(that: Vector2D): Double = ((this.x * that.x) + (this.y * that.y))
@@ -195,6 +202,8 @@ data class Vector2D(val x: Double, val y: Double) : IsAlmostEquals<Vector2D> {
/** DOWN using screen coordinates as reference (0, +1) */
val DOWN_SCREEN = Vector2D(0.0, +1.0)
val INFINITY = Vector2D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)
inline operator fun invoke(x: Number, y: Number): Vector2D = Vector2D(x.toDouble(), y.toDouble())
//inline operator fun invoke(x: Float, y: Float): Vector2D = Vector2D(x.toDouble(), y.toDouble())

View File

@@ -1,5 +1,3 @@
import org.gradle.internal.os.OperatingSystem
rootProject.name = "orx"
@@ -36,7 +34,7 @@ dependencyResolutionManagement {
versionCatalogs {
// We use a regex to get the openrndr version from the primary catalog as there is no public Gradle API to parse catalogs.
val regEx = Regex("^openrndr[ ]*=[ ]*(?:\\{[ ]*require[ ]*=[ ]*)?\"(.*)\"[ ]*(?:\\})?", RegexOption.MULTILINE)
val openrndrVersion = regEx.find(File(rootDir,"gradle/libs.versions.toml").readText())?.groupValues?.get(1) ?: error("can't find openrndr version")
val openrndrVersion = regEx.find(File(rootDir, "gradle/libs.versions.toml").readText())?.groupValues?.get(1) ?: error("can't find openrndr version")
create("sharedLibs") {
from("org.openrndr:openrndr-dependency-catalog:$openrndrVersion")
}
@@ -130,4 +128,5 @@ include(":android")
include(":math")
include(":desktop")
include(":icegps-common")
include(":icegps-shared")
include(":icegps-shared")
include(":icegps-triangulation")