android 去除 orx 相关依赖
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
135
android/src/main/java/com/icegps/orx/catmullrom/CatmullRom.kt
Normal file
135
android/src/main/java/com/icegps/orx/catmullrom/CatmullRom.kt
Normal 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 Catmull–Rom 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)
|
||||
427
android/src/main/java/com/icegps/orx/color/ColorRGBa.kt
Normal file
427
android/src/main/java/com/icegps/orx/color/ColorRGBa.kt
Normal 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)
|
||||
2777
android/src/main/java/com/icegps/orx/colorbrewer2/ColorBrewer2.kt
Normal file
2777
android/src/main/java/com/icegps/orx/colorbrewer2/ColorBrewer2.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
package com.icegps.orx.ktx
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import com.icegps.orx.color.ColorRGBa
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user