Compare commits
2 Commits
0d15c60606
...
terrain
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c90073363 | |||
| 2525d30c80 |
@@ -51,19 +51,10 @@ dependencies {
|
|||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.mapbox.maps)
|
implementation(libs.mapbox.maps)
|
||||||
implementation(project(":math"))
|
implementation(project(":math"))
|
||||||
implementation(project(":orx-triangulation")) {
|
|
||||||
exclude(group = "org.openrndr", module = "openrndr-draw")
|
|
||||||
}
|
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(project(":icegps-common"))
|
implementation(project(":icegps-common"))
|
||||||
implementation(project(":icegps-shared"))
|
implementation(project(":icegps-shared"))
|
||||||
implementation(project(":orx-marching-squares"))
|
implementation(project(":icegps-triangulation"))
|
||||||
implementation(project(":orx-palette")) {
|
|
||||||
exclude(group = "org.openrndr", module = "openrndr-draw")
|
|
||||||
}
|
|
||||||
implementation(project(":orx-shapes")) {
|
|
||||||
exclude(group = "org.openrndr", module = "openrndr-draw")
|
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.ext.junit)
|
androidTestImplementation(libs.ext.junit)
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import ColorBrewer2Type
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import colorBrewer2Palettes
|
import colorBrewer2Palettes
|
||||||
|
import com.icegps.math.geometry.Rectangle
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
import com.icegps.math.geometry.Vector3D
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.orx.catmullrom.CatmullRomChain2
|
||||||
import com.icegps.orx.ktx.area
|
import com.icegps.orx.ktx.area
|
||||||
import com.icegps.orx.ktx.toColorInt
|
import com.icegps.orx.ktx.toColorInt
|
||||||
import com.icegps.orx.ktx.toMapboxPoint
|
import com.icegps.orx.ktx.toMapboxPoint
|
||||||
import com.icegps.orx.ktx.toast
|
import com.icegps.orx.ktx.toast
|
||||||
import com.icegps.orx.triangulation.DelaunayTriangulation3D
|
import com.icegps.orx.marchingsquares.ShapeContour
|
||||||
import com.icegps.orx.triangulation.Triangle3D
|
import com.icegps.orx.marchingsquares.findContours
|
||||||
import com.icegps.shared.ktx.TAG
|
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.Feature
|
||||||
import com.mapbox.geojson.FeatureCollection
|
import com.mapbox.geojson.FeatureCollection
|
||||||
import com.mapbox.geojson.LineString
|
import com.mapbox.geojson.LineString
|
||||||
@@ -28,15 +33,13 @@ import com.mapbox.maps.extension.style.sources.addSource
|
|||||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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
|
import kotlin.math.max
|
||||||
|
|
||||||
class ContoursManager(
|
class ContoursManager(
|
||||||
@@ -54,7 +57,7 @@ class ContoursManager(
|
|||||||
private var contourSize: Int = 6
|
private var contourSize: Int = 6
|
||||||
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
|
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
|
||||||
private var cellSize: Double? = 10.0
|
private var cellSize: Double? = 10.0
|
||||||
private val simplePalette = SimplePalette(
|
val simplePalette = SimplePalette(
|
||||||
range = 0.0..100.0
|
range = 0.0..100.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,18 +106,21 @@ class ContoursManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isGridVisible: Boolean = true
|
private var isGridVisible: Boolean = true
|
||||||
private var gridModel: GridModel? = null
|
private var _gridModel = MutableStateFlow<GridModel?>(null)
|
||||||
|
val gridModel = _gridModel.asStateFlow()
|
||||||
|
|
||||||
fun setGridVisible(visible: Boolean) {
|
fun setGridVisible(visible: Boolean) {
|
||||||
if (visible != isGridVisible) {
|
if (visible != isGridVisible) {
|
||||||
isGridVisible = visible
|
isGridVisible = visible
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (gridModel != null) mapView.displayGridModel(
|
_gridModel.value?.let { gridModel ->
|
||||||
grid = gridModel!!,
|
mapView.displayGridModel(
|
||||||
sourceId = gridSourceId,
|
grid = gridModel,
|
||||||
layerId = gridLayerId,
|
sourceId = gridSourceId,
|
||||||
palette = simplePalette::palette
|
layerId = gridLayerId,
|
||||||
)
|
palette = simplePalette::palette
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mapView.mapboxMap.getStyle { style ->
|
mapView.mapboxMap.getStyle { style ->
|
||||||
try {
|
try {
|
||||||
@@ -130,7 +136,7 @@ class ContoursManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var triangles: List<Triangle3D> = listOf()
|
private var triangles: List<Triangle> = listOf()
|
||||||
private var isTriangleVisible: Boolean = true
|
private var isTriangleVisible: Boolean = true
|
||||||
|
|
||||||
fun setTriangleVisible(visible: Boolean) {
|
fun setTriangleVisible(visible: Boolean) {
|
||||||
@@ -149,22 +155,24 @@ class ContoursManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
val points = points
|
val points = points
|
||||||
if (points.size <= 3) {
|
if (points.size <= 3) {
|
||||||
context.toast("points size ${points.size}")
|
context.toast("points size ${points.size}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
job?.cancel()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
mapView.mapboxMap.getStyle { style ->
|
mapView.mapboxMap.getStyle { style ->
|
||||||
val step = heightRange.endInclusive / contourSize
|
val step = heightRange.endInclusive / contourSize
|
||||||
val zip = (0..contourSize).map { index ->
|
val zip = (0..contourSize).map { index ->
|
||||||
heightRange.start + index * step
|
heightRange.start + index * step
|
||||||
}.zipWithNext { a, b -> a..b }
|
}.zipWithNext { a, b -> a..b }
|
||||||
val points = points.map { Vector3(it.x, it.y, it.z) }
|
|
||||||
val area = points.area
|
val area = points.area
|
||||||
val triangulation = DelaunayTriangulation3D(points)
|
val triangulation = DelaunayTriangulation(points)
|
||||||
val triangles: MutableList<Triangle3D> = triangulation.triangles()
|
val triangles = triangulation.triangles()
|
||||||
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
|
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
|
||||||
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
|
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
|
||||||
} else {
|
} else {
|
||||||
@@ -175,7 +183,7 @@ class ContoursManager(
|
|||||||
delaunator = triangulation,
|
delaunator = triangulation,
|
||||||
cellSize = cellSize,
|
cellSize = cellSize,
|
||||||
)
|
)
|
||||||
this@ContoursManager.gridModel = gridModel
|
this@ContoursManager._gridModel.value = gridModel
|
||||||
if (isGridVisible) mapView.displayGridModel(
|
if (isGridVisible) mapView.displayGridModel(
|
||||||
grid = gridModel,
|
grid = gridModel,
|
||||||
sourceId = gridSourceId,
|
sourceId = gridSourceId,
|
||||||
@@ -183,7 +191,7 @@ class ContoursManager(
|
|||||||
palette = simplePalette::palette
|
palette = simplePalette::palette
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
scope.launch(Dispatchers.Default) {
|
job = scope.launch(Dispatchers.Default) {
|
||||||
val lineFeatures = mutableListOf<List<Feature>>()
|
val lineFeatures = mutableListOf<List<Feature>>()
|
||||||
val features = zip.mapIndexed { index, range ->
|
val features = zip.mapIndexed { index, range ->
|
||||||
async {
|
async {
|
||||||
@@ -219,12 +227,12 @@ class ContoursManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findContours(
|
fun findContours(
|
||||||
triangles: MutableList<Triangle3D>,
|
triangles: List<Triangle>,
|
||||||
range: ClosedFloatingPointRange<Double>,
|
range: ClosedFloatingPointRange<Double>,
|
||||||
area: Rectangle,
|
area: Rectangle,
|
||||||
cellSize: Double
|
cellSize: Double
|
||||||
): List<ShapeContour> {
|
): List<ShapeContour> {
|
||||||
return org.openrndr.extra.marchingsquares.findContours(
|
return findContours(
|
||||||
f = { v ->
|
f = { v ->
|
||||||
val triangle = triangles.firstOrNull { triangle ->
|
val triangle = triangles.firstOrNull { triangle ->
|
||||||
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
|
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||||
@@ -264,10 +272,10 @@ class ContoursManager(
|
|||||||
style.addSource(source)
|
style.addSource(source)
|
||||||
|
|
||||||
val layer = lineLayer(layerId, sourceId) {
|
val layer = lineLayer(layerId, sourceId) {
|
||||||
lineColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
|
lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
|
||||||
lineWidth(1.0)
|
lineWidth(1.0)
|
||||||
lineCap(LineCap.Companion.ROUND)
|
lineCap(LineCap.ROUND)
|
||||||
lineJoin(LineJoin.Companion.ROUND)
|
lineJoin(LineJoin.ROUND)
|
||||||
lineOpacity(0.8)
|
lineOpacity(0.8)
|
||||||
}
|
}
|
||||||
style.addLayer(layer)
|
style.addLayer(layer)
|
||||||
@@ -288,7 +296,7 @@ class ContoursManager(
|
|||||||
style.addSource(source)
|
style.addSource(source)
|
||||||
|
|
||||||
val layer = fillLayer(layerId, sourceId) {
|
val layer = fillLayer(layerId, sourceId) {
|
||||||
fillColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
|
fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||||
fillOpacity(0.5)
|
fillOpacity(0.5)
|
||||||
fillAntialias(true)
|
fillAntialias(true)
|
||||||
}
|
}
|
||||||
@@ -361,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个顶点" }
|
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||||
|
|
||||||
val (v1, v2, v3) = triangle
|
val (v1, v2, v3) = triangle
|
||||||
@@ -386,15 +394,15 @@ fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
|
|||||||
* @param triangle 三角形的三个顶点
|
* @param triangle 三角形的三个顶点
|
||||||
* @return 三维点 (x, y, z)
|
* @return 三维点 (x, y, z)
|
||||||
*/
|
*/
|
||||||
fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
|
fun interpolateHeight(point: Vector2D, triangle: List<Vector3D>): Vector3D {
|
||||||
/**
|
/**
|
||||||
* 计算点在三角形中的重心坐标
|
* 计算点在三角形中的重心坐标
|
||||||
*/
|
*/
|
||||||
fun calculateBarycentricCoordinates(
|
fun calculateBarycentricCoordinates(
|
||||||
point: Vector2,
|
point: Vector2D,
|
||||||
v1: Vector3,
|
v1: Vector3D,
|
||||||
v2: Vector3,
|
v2: Vector3D,
|
||||||
v3: Vector3
|
v3: Vector3D
|
||||||
): Triple<Double, Double, Double> {
|
): Triple<Double, Double, Double> {
|
||||||
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||||
|
|
||||||
@@ -415,5 +423,5 @@ fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
|
|||||||
// 使用重心坐标插值z值
|
// 使用重心坐标插值z值
|
||||||
val z = alpha * v1.z + beta * v2.z + gamma * v3.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)
|
||||||
}
|
}
|
||||||
|
|||||||
197
android/src/main/java/com/icegps/orx/ControllableArrow.kt
Normal file
197
android/src/main/java/com/icegps/orx/ControllableArrow.kt
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Angle
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.orx.ktx.toMapboxPoint
|
||||||
|
import com.mapbox.geojson.Feature
|
||||||
|
import com.mapbox.geojson.FeatureCollection
|
||||||
|
import com.mapbox.geojson.LineString
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
import com.mapbox.geojson.Polygon
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.Style
|
||||||
|
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
||||||
|
import com.mapbox.maps.extension.style.layers.addLayer
|
||||||
|
import com.mapbox.maps.extension.style.layers.generated.FillLayer
|
||||||
|
import com.mapbox.maps.extension.style.layers.generated.LineLayer
|
||||||
|
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 kotlin.math.cos
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置趋势箭头图层
|
||||||
|
*/
|
||||||
|
fun setupTrendLayer(
|
||||||
|
style: Style,
|
||||||
|
trendSourceId: String,
|
||||||
|
trendLayerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
val trendSource = geoJsonSource(trendSourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(trendLayerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer("$trendLayerId-head")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.styleSourceExists(trendSourceId)) {
|
||||||
|
style.removeStyleSource(trendSourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.addSource(trendSource)
|
||||||
|
|
||||||
|
val lineLayer = LineLayer(trendLayerId, trendSourceId).apply {
|
||||||
|
lineColor(Expression.toColor(Expression.get("color")))
|
||||||
|
lineWidth(4.0)
|
||||||
|
lineCap(LineCap.ROUND)
|
||||||
|
lineJoin(LineJoin.ROUND)
|
||||||
|
}
|
||||||
|
style.addLayer(lineLayer)
|
||||||
|
|
||||||
|
val headLayer = FillLayer("$trendLayerId-head", trendSourceId).apply {
|
||||||
|
fillColor(Expression.toColor(Expression.get("color")))
|
||||||
|
}
|
||||||
|
style.addLayer(headLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MapView.displayControllableArrow(
|
||||||
|
grid: GridModel,
|
||||||
|
sourceId: String = "controllable-source-id-0",
|
||||||
|
layerId: String = "controllable-layer-id-0",
|
||||||
|
arrowScale: Double = 0.4,
|
||||||
|
angle: Angle,
|
||||||
|
onHeadArrowChange: (List<Point>) -> Unit
|
||||||
|
) {
|
||||||
|
mapboxMap.getStyle { style ->
|
||||||
|
val centerX = (grid.minX + grid.maxX) / 2
|
||||||
|
val centerY = (grid.minY + grid.maxY) / 2
|
||||||
|
|
||||||
|
val regionWidth = grid.maxX - grid.minX
|
||||||
|
val regionHeight = grid.maxY - grid.minY
|
||||||
|
val arrowLength = min(regionWidth, regionHeight) * arrowScale * 1.0
|
||||||
|
|
||||||
|
val arrowDirectionRad = angle.radians
|
||||||
|
val endX = centerX + sin(arrowDirectionRad) * arrowLength
|
||||||
|
val endY = centerY + cos(arrowDirectionRad) * arrowLength
|
||||||
|
|
||||||
|
val arrowLine = LineString.fromLngLats(
|
||||||
|
listOf(
|
||||||
|
Vector2D(centerX, centerY),
|
||||||
|
Vector2D(endX, endY)
|
||||||
|
).map { it.toMapboxPoint() }
|
||||||
|
)
|
||||||
|
|
||||||
|
val arrowFeature = Feature.fromGeometry(arrowLine)
|
||||||
|
arrowFeature.addStringProperty("color", "#0000FF")
|
||||||
|
arrowFeature.addStringProperty("type", "overall-trend")
|
||||||
|
|
||||||
|
// 创建箭头头部
|
||||||
|
val headSize = arrowLength * 0.2
|
||||||
|
val leftRad = arrowDirectionRad + Math.PI * 0.8
|
||||||
|
val rightRad = arrowDirectionRad - Math.PI * 0.8
|
||||||
|
|
||||||
|
val leftX = endX + sin(leftRad) * headSize
|
||||||
|
val leftY = endY + cos(leftRad) * headSize
|
||||||
|
val rightX = endX + sin(rightRad) * headSize
|
||||||
|
val rightY = endY + cos(rightRad) * headSize
|
||||||
|
|
||||||
|
val headRing = listOf(
|
||||||
|
Vector2D(endX, endY),
|
||||||
|
Vector2D(leftX, leftY),
|
||||||
|
Vector2D(rightX, rightY),
|
||||||
|
Vector2D(endX, endY)
|
||||||
|
).map { it.toMapboxPoint() }
|
||||||
|
onHeadArrowChange(headRing)
|
||||||
|
val headPolygon = Polygon.fromLngLats(listOf(headRing))
|
||||||
|
val headFeature = Feature.fromGeometry(headPolygon)
|
||||||
|
headFeature.addStringProperty("color", "#0000FF")
|
||||||
|
headFeature.addStringProperty("type", "overall-trend")
|
||||||
|
|
||||||
|
val features = listOf(arrowFeature, headFeature)
|
||||||
|
|
||||||
|
// 设置图层
|
||||||
|
setupTrendLayer(style, sourceId, layerId, features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateArrowData(
|
||||||
|
grid: GridModel,
|
||||||
|
angle: Angle,
|
||||||
|
arrowScale: Double = 0.4
|
||||||
|
): ArrowData {
|
||||||
|
val centerX = (grid.minX + grid.maxX) / 2
|
||||||
|
val centerY = (grid.minY + grid.maxY) / 2
|
||||||
|
|
||||||
|
val regionWidth = grid.maxX - grid.minX
|
||||||
|
val regionHeight = grid.maxY - grid.minY
|
||||||
|
val arrowLength = min(regionWidth, regionHeight) * arrowScale * 1.0
|
||||||
|
|
||||||
|
val arrowDirectionRad = angle.radians
|
||||||
|
val endX = centerX + sin(arrowDirectionRad) * arrowLength
|
||||||
|
val endY = centerY + cos(arrowDirectionRad) * arrowLength
|
||||||
|
|
||||||
|
val arrowLine = listOf(
|
||||||
|
Vector2D(centerX, centerY),
|
||||||
|
Vector2D(endX, endY)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建箭头头部
|
||||||
|
val headSize = arrowLength * 0.2
|
||||||
|
val leftRad = arrowDirectionRad + Math.PI * 0.8
|
||||||
|
val rightRad = arrowDirectionRad - Math.PI * 0.8
|
||||||
|
|
||||||
|
val leftX = endX + sin(leftRad) * headSize
|
||||||
|
val leftY = endY + cos(leftRad) * headSize
|
||||||
|
val rightX = endX + sin(rightRad) * headSize
|
||||||
|
val rightY = endY + cos(rightRad) * headSize
|
||||||
|
|
||||||
|
val headRing = listOf(
|
||||||
|
Vector2D(endX, endY),
|
||||||
|
Vector2D(leftX, leftY),
|
||||||
|
Vector2D(rightX, rightY),
|
||||||
|
Vector2D(endX, endY)
|
||||||
|
)
|
||||||
|
return ArrowData(
|
||||||
|
arrowLine = arrowLine,
|
||||||
|
headRing = headRing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ArrowData(
|
||||||
|
val arrowLine: List<Vector2D>,
|
||||||
|
val headRing: List<Vector2D>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun MapView.displayControllableArrow(
|
||||||
|
sourceId: String = "controllable-source-id-0",
|
||||||
|
layerId: String = "controllable-layer-id-0",
|
||||||
|
arrowData: ArrowData
|
||||||
|
) {
|
||||||
|
mapboxMap.getStyle { style ->
|
||||||
|
val (arrowLine, headRing) = arrowData
|
||||||
|
val arrowFeature = Feature.fromGeometry(LineString.fromLngLats(arrowLine.map { it.toMapboxPoint() }))
|
||||||
|
arrowFeature.addStringProperty("color", "#0000FF")
|
||||||
|
arrowFeature.addStringProperty("type", "overall-trend")
|
||||||
|
|
||||||
|
val headPolygon = Polygon.fromLngLats(listOf(headRing.map { it.toMapboxPoint() }))
|
||||||
|
val headFeature = Feature.fromGeometry(headPolygon)
|
||||||
|
headFeature.addStringProperty("color", "#0000FF")
|
||||||
|
headFeature.addStringProperty("type", "overall-trend")
|
||||||
|
|
||||||
|
val features = listOf(arrowFeature, headFeature)
|
||||||
|
|
||||||
|
// 设置图层
|
||||||
|
setupTrendLayer(style, sourceId, layerId, features)
|
||||||
|
}
|
||||||
|
}
|
||||||
144
android/src/main/java/com/icegps/orx/DisplaySlopeResult.kt
Normal file
144
android/src/main/java/com/icegps/orx/DisplaySlopeResult.kt
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.orx.ktx.toMapboxPoint
|
||||||
|
import com.mapbox.geojson.Feature
|
||||||
|
import com.mapbox.geojson.FeatureCollection
|
||||||
|
import com.mapbox.geojson.Polygon
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.Style
|
||||||
|
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
||||||
|
import com.mapbox.maps.extension.style.layers.addLayer
|
||||||
|
import com.mapbox.maps.extension.style.layers.generated.FillLayer
|
||||||
|
import com.mapbox.maps.extension.style.layers.generated.LineLayer
|
||||||
|
import com.mapbox.maps.extension.style.sources.addSource
|
||||||
|
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/26
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 绘制斜坡设计结果
|
||||||
|
*/
|
||||||
|
fun MapView.displaySlopeResult(
|
||||||
|
originalGrid: GridModel,
|
||||||
|
slopeResult: SlopeResult,
|
||||||
|
sourceId: String = "slope-result",
|
||||||
|
layerId: String = "slope-layer",
|
||||||
|
palette: (Double?) -> String,
|
||||||
|
showDesignHeight: Boolean
|
||||||
|
) {
|
||||||
|
val elevationList = mutableListOf<Double>()
|
||||||
|
mapboxMap.getStyle { style ->
|
||||||
|
val features = mutableListOf<Feature>()
|
||||||
|
val designGrid = slopeResult.designSurface
|
||||||
|
|
||||||
|
// 对比测试,将绘制到原来图形的左边
|
||||||
|
// val minX = originalGrid.minX * 2 - originalGrid.maxX
|
||||||
|
val minX = originalGrid.minX
|
||||||
|
val maxY = originalGrid.maxY
|
||||||
|
|
||||||
|
val cellSize = originalGrid.cellSize
|
||||||
|
|
||||||
|
for (r in 0 until originalGrid.rows) {
|
||||||
|
for (c in 0 until originalGrid.cols) {
|
||||||
|
val originalElev = originalGrid.getValue(r, c) ?: continue
|
||||||
|
val designElev = designGrid.getValue(r, c) ?: continue
|
||||||
|
elevationList.add(designElev)
|
||||||
|
|
||||||
|
// 计算填挖高度
|
||||||
|
val heightDiff = designElev - originalElev
|
||||||
|
|
||||||
|
// 计算栅格边界
|
||||||
|
val x0 = minX + c * cellSize
|
||||||
|
val y0 = maxY - r * cellSize
|
||||||
|
val x1 = x0 + cellSize
|
||||||
|
val y1 = y0 - cellSize
|
||||||
|
|
||||||
|
// 1. 创建多边形要素(背景色)
|
||||||
|
val ring = listOf(
|
||||||
|
Vector2D(x0, y0),
|
||||||
|
Vector2D(x1, y0),
|
||||||
|
Vector2D(x1, y1),
|
||||||
|
Vector2D(x0, y1),
|
||||||
|
Vector2D(x0, y0)
|
||||||
|
).map { it.toMapboxPoint() }
|
||||||
|
val poly = Polygon.fromLngLats(listOf(ring))
|
||||||
|
val feature = Feature.fromGeometry(poly)
|
||||||
|
|
||||||
|
if (showDesignHeight) {
|
||||||
|
// 显示设计高度,测试坡向是否正确,和高度是否计算正确
|
||||||
|
feature.addStringProperty("color", palette(designElev))
|
||||||
|
} else {
|
||||||
|
// 显示高差
|
||||||
|
feature.addStringProperty("color", palette(heightDiff))
|
||||||
|
}
|
||||||
|
// 显示原始高度
|
||||||
|
// feature.addStringProperty("color", palette(originalElev))
|
||||||
|
features.add(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("displayGridWithDirectionArrows", "对比区域的土方量计算: ${elevationList.sum()}, 平均值:${elevationList.average()}")
|
||||||
|
|
||||||
|
// 设置图层
|
||||||
|
setupEarthworkLayer(style, sourceId, layerId, features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整的土方工程图层设置 - 修正版
|
||||||
|
*/
|
||||||
|
private fun setupEarthworkLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>,
|
||||||
|
) {
|
||||||
|
// 创建数据源
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧图层
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer("$layerId-arrow")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer("$layerId-outline")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer("$layerId-text")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.styleSourceExists(sourceId)) {
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加数据源
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
// 主填充图层
|
||||||
|
val fillLayer = FillLayer(layerId, sourceId).apply {
|
||||||
|
fillColor(Expression.toColor(Expression.get("color")))
|
||||||
|
fillOpacity(0.7)
|
||||||
|
}
|
||||||
|
style.addLayer(fillLayer)
|
||||||
|
|
||||||
|
// 边框图层
|
||||||
|
val outlineLayer = LineLayer("$layerId-outline", sourceId).apply {
|
||||||
|
lineColor("#333333")
|
||||||
|
lineWidth(1.0)
|
||||||
|
lineOpacity(0.5)
|
||||||
|
}
|
||||||
|
style.addLayer(outlineLayer)
|
||||||
|
}
|
||||||
438
android/src/main/java/com/icegps/orx/EarthworkManager.kt
Normal file
438
android/src/main/java/com/icegps/orx/EarthworkManager.kt
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.util.Log
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Angle
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.math.geometry.degrees
|
||||||
|
import com.icegps.shared.ktx.TAG
|
||||||
|
import com.mapbox.android.gestures.MoveGestureDetector
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.ScreenCoordinate
|
||||||
|
import com.mapbox.maps.plugin.gestures.OnMoveListener
|
||||||
|
import com.mapbox.maps.plugin.gestures.addOnMoveListener
|
||||||
|
import com.mapbox.maps.plugin.gestures.removeOnMoveListener
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/26
|
||||||
|
*/
|
||||||
|
object SlopeCalculator {
|
||||||
|
fun calculateSlope(
|
||||||
|
grid: GridModel,
|
||||||
|
slopeDirection: Double,
|
||||||
|
slopePercentage: Double,
|
||||||
|
baseHeightOffset: Double = 0.0
|
||||||
|
): SlopeResult {
|
||||||
|
val centerX = (grid.minX + grid.maxX) / 2
|
||||||
|
val centerY = (grid.minY + grid.maxY) / 2
|
||||||
|
|
||||||
|
val elevations = grid.cells.filterNotNull()
|
||||||
|
val baseElevation = elevations.average() + baseHeightOffset
|
||||||
|
|
||||||
|
val basePoint = Triple(centerX, centerY, baseElevation)
|
||||||
|
|
||||||
|
val earthworkResult = EarthworkCalculator.calculateForSlopeDesign(
|
||||||
|
grid = grid,
|
||||||
|
basePoint = basePoint,
|
||||||
|
slope = slopePercentage,
|
||||||
|
aspect = slopeDirection
|
||||||
|
)
|
||||||
|
|
||||||
|
return SlopeResult(
|
||||||
|
slopeDirection = slopeDirection,
|
||||||
|
slopePercentage = slopePercentage,
|
||||||
|
baseHeightOffset = baseHeightOffset,
|
||||||
|
baseElevation = baseElevation,
|
||||||
|
earthworkResult = earthworkResult,
|
||||||
|
designSurface = generateSlopeDesignGrid(
|
||||||
|
grid = grid,
|
||||||
|
basePoint = basePoint,
|
||||||
|
slopePercentage = slopePercentage,
|
||||||
|
slopeDirection = slopeDirection
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成斜坡设计面网格(用于可视化)
|
||||||
|
*/
|
||||||
|
private fun generateSlopeDesignGrid(
|
||||||
|
grid: GridModel,
|
||||||
|
basePoint: Triple<Double, Double, Double>,
|
||||||
|
slopePercentage: Double,
|
||||||
|
slopeDirection: Double
|
||||||
|
): GridModel {
|
||||||
|
val designCells = Array<Double?>(grid.rows * grid.cols) { null }
|
||||||
|
val (baseX, baseY, baseElev) = basePoint
|
||||||
|
val slopeRatio = slopePercentage / 100.0
|
||||||
|
|
||||||
|
for (r in 0 until grid.rows) {
|
||||||
|
for (c in 0 until grid.cols) {
|
||||||
|
if (grid.getValue(r, c) != null) {
|
||||||
|
val cellX = grid.minX + (c + 0.5) * (grid.maxX - grid.minX) / grid.cols
|
||||||
|
val cellY = grid.minY + (r + 0.5) * (grid.maxY - grid.minY) / grid.rows
|
||||||
|
|
||||||
|
val designElev = calculateSlopeElevation(
|
||||||
|
pointX = cellX,
|
||||||
|
pointY = cellY,
|
||||||
|
baseX = baseX,
|
||||||
|
baseY = baseY,
|
||||||
|
baseElev = baseElev,
|
||||||
|
slopeRatio = slopeRatio,
|
||||||
|
slopeDirection = slopeDirection
|
||||||
|
)
|
||||||
|
designCells[r * grid.cols + c] = designElev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GridModel(
|
||||||
|
minX = grid.minX,
|
||||||
|
maxX = grid.maxX,
|
||||||
|
minY = grid.minY,
|
||||||
|
maxY = grid.maxY,
|
||||||
|
rows = grid.rows,
|
||||||
|
cols = grid.cols,
|
||||||
|
cellSize = grid.cellSize,
|
||||||
|
cells = designCells
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 斜坡高程计算
|
||||||
|
*/
|
||||||
|
fun calculateSlopeElevation(
|
||||||
|
pointX: Double,
|
||||||
|
pointY: Double,
|
||||||
|
baseX: Double,
|
||||||
|
baseY: Double,
|
||||||
|
baseElev: Double,
|
||||||
|
slopeRatio: Double,
|
||||||
|
slopeDirection: Double
|
||||||
|
): Double {
|
||||||
|
val dx = (pointX - baseX) * cos(Math.toRadians(baseY))
|
||||||
|
val dy = (pointY - baseY)
|
||||||
|
|
||||||
|
val slopeRad = (slopeDirection.degrees - 90.degrees).normalized.radians
|
||||||
|
|
||||||
|
val projection = dx * cos(slopeRad) + dy * sin(slopeRad)
|
||||||
|
val heightDiff = projection * slopeRatio
|
||||||
|
|
||||||
|
return baseElev + heightDiff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 斜面设计
|
||||||
|
*
|
||||||
|
* @property slopeDirection 坡向 (度)
|
||||||
|
* @property slopePercentage 坡度 (%)
|
||||||
|
* @property baseHeightOffset 基准面高度偏移 (m)
|
||||||
|
* @property baseElevation 基准点高程 (m)
|
||||||
|
* @property earthworkResult 土方量结果
|
||||||
|
* @property designSurface 设计面网格(用于可视化)
|
||||||
|
*/
|
||||||
|
data class SlopeResult(
|
||||||
|
val slopeDirection: Double,
|
||||||
|
val slopePercentage: Double,
|
||||||
|
val baseHeightOffset: Double,
|
||||||
|
val baseElevation: Double,
|
||||||
|
val earthworkResult: EarthworkResult,
|
||||||
|
val designSurface: GridModel
|
||||||
|
)
|
||||||
|
|
||||||
|
object EarthworkCalculator {
|
||||||
|
/**
|
||||||
|
* @param grid 栅格网模型
|
||||||
|
* @param designElevation 设计高程
|
||||||
|
*/
|
||||||
|
fun calculateForFlatDesign(
|
||||||
|
grid: GridModel,
|
||||||
|
designElevation: Double
|
||||||
|
): EarthworkResult {
|
||||||
|
var cutVolume = 0.0
|
||||||
|
var fillVolume = 0.0
|
||||||
|
var cutArea = 0.0
|
||||||
|
var fillArea = 0.0
|
||||||
|
val cellArea = grid.cellSize * grid.cellSize
|
||||||
|
|
||||||
|
for (r in 0 until grid.rows) {
|
||||||
|
for (c in 0 until grid.cols) {
|
||||||
|
val originalElev = grid.getValue(r, c) ?: continue
|
||||||
|
|
||||||
|
val heightDiff = designElevation - originalElev
|
||||||
|
|
||||||
|
val volume = heightDiff * cellArea
|
||||||
|
|
||||||
|
if (volume > 0) {
|
||||||
|
fillVolume += volume
|
||||||
|
fillArea += cellArea
|
||||||
|
} else if (volume < 0) {
|
||||||
|
cutVolume += abs(volume)
|
||||||
|
cutArea += cellArea
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EarthworkResult(
|
||||||
|
cutVolume = cutVolume,
|
||||||
|
fillVolume = fillVolume,
|
||||||
|
netVolume = fillVolume - cutVolume,
|
||||||
|
cutArea = cutArea,
|
||||||
|
fillArea = fillArea,
|
||||||
|
totalArea = cutArea + fillArea
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算斜面设计的土方量
|
||||||
|
*/
|
||||||
|
fun calculateForSlopeDesign(
|
||||||
|
grid: GridModel,
|
||||||
|
basePoint: Triple<Double, Double, Double>,
|
||||||
|
slope: Double,
|
||||||
|
aspect: Double
|
||||||
|
): EarthworkResult {
|
||||||
|
var cutVolume = 0.0
|
||||||
|
var fillVolume = 0.0
|
||||||
|
var cutArea = 0.0
|
||||||
|
var fillArea = 0.0
|
||||||
|
val cellArea = grid.cellSize * grid.cellSize
|
||||||
|
|
||||||
|
val (baseX, baseY, baseElev) = basePoint
|
||||||
|
val slopeRatio = slope / 100.0
|
||||||
|
|
||||||
|
for (r in 0 until grid.rows) {
|
||||||
|
for (c in 0 until grid.cols) {
|
||||||
|
val originalElev = grid.getValue(r, c) ?: continue
|
||||||
|
|
||||||
|
val cellX = grid.minX + (c + 0.5) * (grid.maxX - grid.minX) / grid.cols
|
||||||
|
val cellY = grid.minY + (r + 0.5) * (grid.maxY - grid.minY) / grid.rows
|
||||||
|
|
||||||
|
val designElev = SlopeCalculator.calculateSlopeElevation(
|
||||||
|
pointX = cellX,
|
||||||
|
pointY = cellY,
|
||||||
|
baseX = baseX,
|
||||||
|
baseY = baseY,
|
||||||
|
baseElev = baseElev,
|
||||||
|
slopeRatio = slopeRatio,
|
||||||
|
slopeDirection = aspect
|
||||||
|
)
|
||||||
|
|
||||||
|
val heightElev = designElev - originalElev
|
||||||
|
val volume = heightElev * cellArea
|
||||||
|
|
||||||
|
if (volume > 0) {
|
||||||
|
fillVolume += volume
|
||||||
|
fillArea += cellArea
|
||||||
|
} else if (volume < 0) {
|
||||||
|
cutVolume += abs(volume)
|
||||||
|
cutArea += cellArea
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EarthworkResult(
|
||||||
|
cutVolume = cutVolume,
|
||||||
|
fillVolume = fillVolume,
|
||||||
|
netVolume = fillVolume - cutVolume,
|
||||||
|
cutArea = cutArea,
|
||||||
|
fillArea = fillArea,
|
||||||
|
totalArea = cutArea + fillArea
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 土方量计算结果
|
||||||
|
* @property cutVolume 挖方量 (m³)
|
||||||
|
* @property fillVolume 填方量 (m³)
|
||||||
|
* @property netVolume 净土方量 (m³)
|
||||||
|
* @property cutArea 挖方面积 (m²)
|
||||||
|
* @property fillArea 填方面积 (m²)
|
||||||
|
* @property totalArea 总面积 (m²)
|
||||||
|
*/
|
||||||
|
data class EarthworkResult(
|
||||||
|
val cutVolume: Double,
|
||||||
|
val fillVolume: Double,
|
||||||
|
val netVolume: Double,
|
||||||
|
val cutArea: Double,
|
||||||
|
val fillArea: Double,
|
||||||
|
val totalArea: Double
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("EarthworkResult")
|
||||||
|
appendLine("挖方: ${"%.1f".format(cutVolume)} m³")
|
||||||
|
appendLine("填方: ${"%.1f".format(fillVolume)} m³")
|
||||||
|
appendLine("净土方: ${"%.1f".format(netVolume)} m³")
|
||||||
|
appendLine("挖方面积: ${"%.1f".format(cutArea)} m²")
|
||||||
|
appendLine("填方面积: ${"%.1f".format(fillArea)} m²")
|
||||||
|
appendLine("总面积:${"%.1f".format(totalArea)} m²")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EarthworkManager(
|
||||||
|
private val mapView: MapView,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
private val arrowSourceId: String = "controllable-source-id-0"
|
||||||
|
private val arrowLayerId: String = "controllable-layer-id-0"
|
||||||
|
private var listener: OnMoveListener? = null
|
||||||
|
|
||||||
|
private var gridModel = MutableStateFlow<GridModel?>(null)
|
||||||
|
private val arrowHead = MutableStateFlow(emptyList<Vector2D>())
|
||||||
|
private var arrowCenter = MutableStateFlow(Vector2D(0.0, 0.0))
|
||||||
|
private var arrowEnd = MutableStateFlow(Vector2D(0.0, 1.0))
|
||||||
|
private var _slopeDirection = MutableStateFlow(0.degrees)
|
||||||
|
val slopeDirection = _slopeDirection.asStateFlow()
|
||||||
|
private val _slopePercentage = MutableStateFlow(90.0)
|
||||||
|
val slopePercentage = _slopePercentage.asStateFlow()
|
||||||
|
private val _baseHeightOffset = MutableStateFlow(0.0)
|
||||||
|
val baseHeightOffset = _baseHeightOffset.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
combine(
|
||||||
|
arrowCenter,
|
||||||
|
arrowEnd,
|
||||||
|
gridModel
|
||||||
|
) { center, arrow, gridModel ->
|
||||||
|
gridModel?.let { gridModel ->
|
||||||
|
// _slopeDirection.value = angle
|
||||||
|
displayControllableArrow(gridModel, getSlopeDirection(arrow, center))
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
combine(
|
||||||
|
_slopeDirection,
|
||||||
|
gridModel
|
||||||
|
) { slopeDirection, gridModel ->
|
||||||
|
gridModel?.let {
|
||||||
|
displayControllableArrow(it, slopeDirection)
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSlopeDirection(
|
||||||
|
arrow: Vector2D,
|
||||||
|
center: Vector2D
|
||||||
|
): Angle {
|
||||||
|
val direction = (arrow - center)
|
||||||
|
val atan2 = Angle.atan2(direction.x, direction.y, Vector2D.UP)
|
||||||
|
val angle = atan2.normalized
|
||||||
|
return angle
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayControllableArrow(gridModel: GridModel, slopeDirection: Angle) {
|
||||||
|
val arrowData = calculateArrowData(
|
||||||
|
grid = gridModel,
|
||||||
|
angle = slopeDirection,
|
||||||
|
)
|
||||||
|
arrowHead.value = arrowData.headRing
|
||||||
|
mapView.displayControllableArrow(
|
||||||
|
sourceId = arrowSourceId,
|
||||||
|
layerId = arrowLayerId,
|
||||||
|
arrowData = arrowData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Point.toVector2D(): Vector2D {
|
||||||
|
val geoHelper = GeoHelper.getSharedInstance()
|
||||||
|
val enu = geoHelper.wgs84ToENU(lon = longitude(), lat = latitude(), hgt = 0.0)
|
||||||
|
return Vector2D(enu.x, enu.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeOnMoveListener() {
|
||||||
|
listener?.let(mapView.mapboxMap::removeOnMoveListener)
|
||||||
|
listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupOnMoveListener() {
|
||||||
|
listener = object : OnMoveListener {
|
||||||
|
private var beginning: Boolean = false
|
||||||
|
private var isDragging: Boolean = false
|
||||||
|
private fun getCoordinate(focalPoint: PointF): Point {
|
||||||
|
return mapView.mapboxMap.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(detector: MoveGestureDetector): Boolean {
|
||||||
|
val focalPoint = detector.focalPoint
|
||||||
|
val point = mapView.mapboxMap
|
||||||
|
.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||||
|
.toVector2D()
|
||||||
|
|
||||||
|
val isPointInPolygon = RayCastingAlgorithm.isPointInPolygon(
|
||||||
|
point = point,
|
||||||
|
polygon = arrowHead.value
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isPointInPolygon) {
|
||||||
|
isDragging = true
|
||||||
|
}
|
||||||
|
if (isDragging) {
|
||||||
|
arrowEnd.value = point
|
||||||
|
}
|
||||||
|
return isDragging
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoveBegin(detector: MoveGestureDetector) {
|
||||||
|
Log.d(TAG, "onMoveBegin: $detector")
|
||||||
|
beginning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoveEnd(detector: MoveGestureDetector) {
|
||||||
|
Log.d(TAG, "onMoveEnd: $detector")
|
||||||
|
val point = getCoordinate(detector.focalPoint)
|
||||||
|
val arrow = point.toVector2D()
|
||||||
|
if (beginning && isDragging) {
|
||||||
|
arrowEnd.value = arrow
|
||||||
|
val center = arrowCenter.value
|
||||||
|
_slopeDirection.value = getSlopeDirection(arrow, center)
|
||||||
|
}
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
buildString {
|
||||||
|
appendLine("onMoveEnd: ")
|
||||||
|
appendLine("${point.longitude()}, ${point.latitude()}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
isDragging = false
|
||||||
|
beginning = false
|
||||||
|
}
|
||||||
|
}.also(mapView.mapboxMap::addOnMoveListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGridModel(gridModel: GridModel) {
|
||||||
|
this.gridModel.value = gridModel
|
||||||
|
calculateArrowCenter(gridModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateArrowCenter(gridModel: GridModel) {
|
||||||
|
val centerX = (gridModel.minX + gridModel.maxX) / 2
|
||||||
|
val centerY = (gridModel.minY + gridModel.maxY) / 2
|
||||||
|
arrowCenter.value = Vector2D(centerX, centerY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSlopeDirection(angle: Angle) {
|
||||||
|
_slopeDirection.value = angle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSlopePercentage(value: Double) {
|
||||||
|
_slopePercentage.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDesignHeight(value: Double) {
|
||||||
|
_baseHeightOffset.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.icegps.orx
|
package com.icegps.orx
|
||||||
|
|
||||||
import com.icegps.math.geometry.Vector2D
|
import com.icegps.math.geometry.Vector2D
|
||||||
import com.icegps.orx.triangulation.DelaunayTriangulation3D
|
import com.icegps.math.geometry.Vector3D
|
||||||
import org.openrndr.math.Vector3
|
import com.icegps.triangulation.DelaunayTriangulation
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
@@ -19,14 +19,21 @@ data class GridModel(
|
|||||||
val cols: Int,
|
val cols: Int,
|
||||||
val cellSize: Double,
|
val cellSize: Double,
|
||||||
val cells: Array<Double?>
|
val cells: Array<Double?>
|
||||||
)
|
) {
|
||||||
|
fun getValue(row: Int, col: Int): Double? {
|
||||||
|
if (row !in 0..<rows || col < 0 || col >= cols) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return cells[row * cols + col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun triangulationToGrid(
|
fun triangulationToGrid(
|
||||||
delaunator: DelaunayTriangulation3D,
|
delaunator: DelaunayTriangulation,
|
||||||
cellSize: Double = 50.0,
|
cellSize: Double = 50.0,
|
||||||
maxSidePixels: Int = 5000
|
maxSidePixels: Int = 5000
|
||||||
): GridModel {
|
): 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 v0x = c.x - a.x
|
||||||
val v0y = c.y - a.y
|
val v0y = c.y - a.y
|
||||||
val v1x = b.x - a.x
|
val v1x = b.x - a.x
|
||||||
@@ -48,11 +55,11 @@ fun triangulationToGrid(
|
|||||||
return u >= 0 && v >= 0 && u + v <= 1
|
return u >= 0 && v >= 0 && u + v <= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3, b: Vector3, c: Vector3, values: DoubleArray): Double {
|
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D, values: DoubleArray): Double {
|
||||||
val area = { p1: Vector2D, p2: Vector3, p3: Vector3 ->
|
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
|
((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
|
((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)
|
val areaTotal = area2(a, b, c)
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.google.android.material.slider.RangeSlider
|
import com.google.android.material.slider.RangeSlider
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.icegps.common.helper.GeoHelper
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.degrees
|
||||||
import com.icegps.orx.databinding.ActivityMainBinding
|
import com.icegps.orx.databinding.ActivityMainBinding
|
||||||
import com.icegps.shared.model.GeoPoint
|
import com.icegps.shared.model.GeoPoint
|
||||||
import com.mapbox.geojson.Point
|
import com.mapbox.geojson.Point
|
||||||
import com.mapbox.maps.CameraOptions
|
import com.mapbox.maps.CameraOptions
|
||||||
import com.mapbox.maps.MapView
|
import com.mapbox.maps.MapView
|
||||||
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
|
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
@@ -26,6 +30,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
ViewModelProvider(this)[MainViewModel::class.java]
|
ViewModelProvider(this)[MainViewModel::class.java]
|
||||||
}
|
}
|
||||||
private lateinit var contoursManager: ContoursManager
|
private lateinit var contoursManager: ContoursManager
|
||||||
|
private lateinit var earthworkManager: EarthworkManager
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initGeoHelper()
|
initGeoHelper()
|
||||||
@@ -36,6 +41,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
mapView = binding.mapView
|
mapView = binding.mapView
|
||||||
|
earthworkManager = EarthworkManager(mapView, lifecycleScope)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
@@ -52,27 +58,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
val points = coordinateGenerate1()
|
val points = coordinateGenerate()
|
||||||
|
|
||||||
val polygonTest = PolygonTest(mapView)
|
|
||||||
polygonTest.clear()
|
|
||||||
val innerPoints = points.map { it[0] }
|
|
||||||
val outerPoints = points.map { it[1] }
|
|
||||||
if (false) polygonTest.update(
|
|
||||||
outer = outerPoints,
|
|
||||||
inner = innerPoints,
|
|
||||||
other = points.map { it[2] }
|
|
||||||
)
|
|
||||||
// divider
|
// divider
|
||||||
contoursManager = ContoursManager(
|
contoursManager = ContoursManager(
|
||||||
context = this,
|
context = this,
|
||||||
mapView = mapView,
|
mapView = mapView,
|
||||||
scope = lifecycleScope
|
scope = lifecycleScope
|
||||||
)
|
)
|
||||||
val points2 = points.flatten()
|
|
||||||
contoursManager.updateContourSize(6)
|
contoursManager.updateContourSize(6)
|
||||||
contoursManager.updatePoints(points2)
|
contoursManager.updatePoints(points)
|
||||||
val height = points2.map { it.z }
|
val height = points.map { it.z }
|
||||||
val min = height.min()
|
val min = height.min()
|
||||||
val max = height.max()
|
val max = height.max()
|
||||||
contoursManager.updateHeightRange((min / 2)..max)
|
contoursManager.updateHeightRange((min / 2)..max)
|
||||||
@@ -125,6 +121,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
contoursManager.updateCellSize(slider.value.toDouble())
|
contoursManager.updateCellSize(slider.value.toDouble())
|
||||||
|
contoursManager.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -135,13 +132,78 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.clearPoints.setOnClickListener {
|
binding.clearPoints.setOnClickListener {
|
||||||
viewModel.clearPoints()
|
viewModel.clearPoints()
|
||||||
}
|
}
|
||||||
|
binding.slopeDirection.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
earthworkManager.updateSlopeDirection(slider.value.degrees)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.slopePercentage.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
earthworkManager.updateSlopePercentage(slider.value.toDouble())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.designHeight.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
earthworkManager.updateDesignHeight(slider.value.toDouble())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
binding.switchDesignSurface.setOnCheckedChangeListener { button, isChecked ->
|
||||||
|
showDesignHeight.value = isChecked
|
||||||
|
}
|
||||||
|
earthworkManager.setupOnMoveListener()
|
||||||
initData()
|
initData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val showDesignHeight = MutableStateFlow(false)
|
||||||
|
|
||||||
private fun initData() {
|
private fun initData() {
|
||||||
viewModel.points.onEach {
|
viewModel.points.onEach {
|
||||||
contoursManager.updatePoints(it)
|
contoursManager.updatePoints(it)
|
||||||
contoursManager.updateHeightRange()
|
contoursManager.updateHeightRange()
|
||||||
|
contoursManager.refresh()
|
||||||
|
}.launchIn(lifecycleScope)
|
||||||
|
contoursManager.gridModel.filterNotNull().onEach {
|
||||||
|
earthworkManager.updateGridModel(it)
|
||||||
|
}.launchIn(lifecycleScope)
|
||||||
|
earthworkManager.slopeDirection.onEach {
|
||||||
|
binding.slopeDirection.value = it.degrees.toFloat()
|
||||||
|
}.launchIn(lifecycleScope)
|
||||||
|
combine(
|
||||||
|
earthworkManager.slopeDirection,
|
||||||
|
earthworkManager.slopePercentage,
|
||||||
|
earthworkManager.baseHeightOffset,
|
||||||
|
contoursManager.gridModel,
|
||||||
|
showDesignHeight
|
||||||
|
) { slopeDirection, slopePercentage, baseHeightOffset, gridModel, showDesignHeight ->
|
||||||
|
gridModel?.let { gridModel ->
|
||||||
|
val slopeResult: SlopeResult = SlopeCalculator.calculateSlope(
|
||||||
|
grid = gridModel,
|
||||||
|
slopeDirection = slopeDirection.degrees,
|
||||||
|
slopePercentage = slopePercentage,
|
||||||
|
baseHeightOffset = baseHeightOffset
|
||||||
|
)
|
||||||
|
mapView.displaySlopeResult(
|
||||||
|
originalGrid = gridModel,
|
||||||
|
slopeResult = slopeResult,
|
||||||
|
palette = contoursManager.simplePalette::palette,
|
||||||
|
showDesignHeight = showDesignHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
}.launchIn(lifecycleScope)
|
}.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.layers.properties.generated.LineJoin
|
||||||
import com.mapbox.maps.extension.style.sources.addSource
|
import com.mapbox.maps.extension.style.sources.addSource
|
||||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||||
import org.openrndr.math.YPolarity
|
|
||||||
|
|
||||||
class PolylineManager(
|
class PolylineManager(
|
||||||
private val mapView: MapView
|
private val mapView: MapView
|
||||||
@@ -99,7 +98,6 @@ class PolylineManager(
|
|||||||
fun fromPoints(
|
fun fromPoints(
|
||||||
points: List<Vector3D>,
|
points: List<Vector3D>,
|
||||||
closed: Boolean,
|
closed: Boolean,
|
||||||
polarity: YPolarity = YPolarity.CW_NEGATIVE_Y
|
|
||||||
) = if (points.isEmpty()) {
|
) = if (points.isEmpty()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
44
android/src/main/java/com/icegps/orx/RayCastingAlgorithm.kt
Normal file
44
android/src/main/java/com/icegps/orx/RayCastingAlgorithm.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.math.geometry.toVector2D
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/26
|
||||||
|
*/
|
||||||
|
object RayCastingAlgorithm {
|
||||||
|
/**
|
||||||
|
* 使用射线法判断点是否在多边形内
|
||||||
|
* @param point 测试点
|
||||||
|
* @param polygon 多边形顶点列表
|
||||||
|
* @return true如果在多边形内
|
||||||
|
*/
|
||||||
|
fun isPointInPolygon(point: Vector2D, polygon: List<Vector2D>): Boolean {
|
||||||
|
if (polygon.size < 3) return false
|
||||||
|
|
||||||
|
val x = point.x
|
||||||
|
val y = point.y
|
||||||
|
var inside = false
|
||||||
|
|
||||||
|
var j = polygon.size - 1
|
||||||
|
for (i in polygon.indices) {
|
||||||
|
val xi = polygon[i].x
|
||||||
|
val yi = polygon[i].y
|
||||||
|
val xj = polygon[j].x
|
||||||
|
val yj = polygon[j].y
|
||||||
|
|
||||||
|
val intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
|
||||||
|
|
||||||
|
if (intersect) inside = !inside
|
||||||
|
j = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPointInPolygon(point: Vector3D, polygon: List<Vector3D>): Boolean {
|
||||||
|
return isPointInPolygon(point.toVector2D(), polygon.map { it.toVector2D() })
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
package com.icegps.orx.ktx
|
||||||
|
|
||||||
import org.openrndr.color.ColorRGBa
|
import com.icegps.orx.color.ColorRGBa
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author tabidachinokaze
|
* @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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
android/src/main/java/com/icegps/orx/ktx/Vector2D.kt
Normal file
24
android/src/main/java/com/icegps/orx/ktx/Vector2D.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.icegps.orx.ktx
|
||||||
|
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/26
|
||||||
|
*/
|
||||||
|
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.Companion.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
|
|
||||||
}
|
|
||||||
@@ -112,6 +112,72 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="清除所有点" />
|
android:text="清除所有点" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坡向(角度):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slope_direction"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="360" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坡度(%):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slope_percentage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设计面高度(m):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/design_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:value="0"
|
||||||
|
android:valueFrom="-100"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_design_surface"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="显示设计面" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -103,6 +103,72 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="清除所有点" />
|
android:text="清除所有点" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坡向(角度):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slope_direction"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="360" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坡度(%):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slope_percentage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设计面高度(m):" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/design_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:value="0"
|
||||||
|
android:valueFrom="-100"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_design_surface"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="显示设计面" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.mapbox.maps.MapView
|
<com.mapbox.maps.MapView
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
}
|
}
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":math"))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.openrndr.extra.triangulation
|
package com.icegps.triangulation
|
||||||
|
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package org.openrndr.extra.triangulation
|
package com.icegps.triangulation
|
||||||
|
|
||||||
import org.openrndr.extra.triangulation.Delaunay.Companion.from
|
import com.icegps.math.geometry.Vector2D
|
||||||
import org.openrndr.math.Vector2
|
import com.icegps.triangulation.Delaunay.Companion.from
|
||||||
import org.openrndr.shape.Rectangle
|
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
@@ -42,7 +41,7 @@ class Delaunay(val points: DoubleArray) {
|
|||||||
*
|
*
|
||||||
* @property points a list of 2D points
|
* @property points a list of 2D points
|
||||||
*/
|
*/
|
||||||
fun from(points: List<Vector2>): Delaunay {
|
fun from(points: List<Vector2D>): Delaunay {
|
||||||
val n = points.size
|
val n = points.size
|
||||||
val coords = DoubleArray(n * 2)
|
val coords = DoubleArray(n * 2)
|
||||||
|
|
||||||
@@ -74,13 +73,13 @@ class Delaunay(val points: DoubleArray) {
|
|||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun neighbors(i:Int) = sequence<Int> {
|
fun neighbors(i: Int) = sequence<Int> {
|
||||||
val e0 = inedges[i]
|
val e0 = inedges[i]
|
||||||
if (e0 != -1) {
|
if (e0 != -1) {
|
||||||
var e = e0
|
var e = e0
|
||||||
var p0 = -1
|
var p0 = -1
|
||||||
|
|
||||||
loop@do {
|
loop@ do {
|
||||||
p0 = triangles[e]
|
p0 = triangles[e]
|
||||||
yield(p0)
|
yield(p0)
|
||||||
e = if (e % 3 == 2) e - 2 else e + 1
|
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) {
|
for (i in 0 until triangles.size step 3) {
|
||||||
val a = 2 * triangles[i]
|
val a = 2 * triangles[i]
|
||||||
val b = 2 * triangles[i + 1]
|
val b = 2 * triangles[i + 1]
|
||||||
val c = 2 * triangles[i + 2]
|
val c = 2 * triangles[i + 2]
|
||||||
val coords = points
|
val coords = points
|
||||||
val cross = (coords[c] - coords[a]) * (coords[b + 1] - coords[a + 1])
|
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;
|
if (cross > 1e-10) return false;
|
||||||
}
|
}
|
||||||
return true
|
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() {
|
fun init() {
|
||||||
|
|
||||||
if (hull.size > 2 && collinear()) {
|
if (hull.size > 2 && collinear()) {
|
||||||
println("warning: triangulation is collinear")
|
println("warning: triangulation is collinear")
|
||||||
val r = 1E-8
|
val r = 1E-8
|
||||||
for (i in 0 until points.size step 2) {
|
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] = p[0]
|
||||||
points[i+1] = p[1]
|
points[i + 1] = p[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
delaunator = Delaunator(points)
|
delaunator = Delaunator(points)
|
||||||
@@ -221,12 +222,4 @@ class Delaunay(val points: DoubleArray) {
|
|||||||
|
|
||||||
return c
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
package org.openrndr.extra.triangulation
|
package com.icegps.triangulation
|
||||||
|
|
||||||
import org.openrndr.math.Vector2
|
import com.icegps.math.geometry.Vector3D
|
||||||
import org.openrndr.shape.Rectangle
|
import com.icegps.math.geometry.toVector2D
|
||||||
import org.openrndr.shape.Triangle
|
|
||||||
import org.openrndr.shape.contour
|
|
||||||
import org.openrndr.shape.contours
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kotlin/OPENRNDR idiomatic interface to `Delaunay`
|
* Kotlin/OPENRNDR idiomatic interface to `Delaunay`
|
||||||
*/
|
*/
|
||||||
class DelaunayTriangulation(val points: List<Vector2>) {
|
class DelaunayTriangulation(val points: List<Vector3D>) {
|
||||||
val delaunay: Delaunay = Delaunay.from(points)
|
val delaunay: Delaunay = Delaunay.from(points.map { it.toVector2D() })
|
||||||
|
|
||||||
fun voronoiDiagram(bounds: Rectangle) = VoronoiDiagram(this, bounds)
|
|
||||||
|
|
||||||
fun neighbors(pointIndex: Int): Sequence<Int> {
|
fun neighbors(pointIndex: Int): Sequence<Int> {
|
||||||
return delaunay.neighbors(pointIndex)
|
return delaunay.neighbors(pointIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun neighborPoints(pointIndex: Int): List<Vector2> {
|
fun neighborPoints(pointIndex: Int): List<Vector3D> {
|
||||||
return neighbors(pointIndex).map { points[it] }.toList()
|
return neighbors(pointIndex).map { points[it] }.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,30 +50,9 @@ class DelaunayTriangulation(val points: List<Vector2>) {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner edges of the delaunay triangulation (without hull)
|
fun nearest(query: Vector3D): Int = delaunay.find(query.x, query.y)
|
||||||
fun halfedges() = contours {
|
|
||||||
for (i in delaunay.halfedges.indices) {
|
|
||||||
val j = delaunay.halfedges[i]
|
|
||||||
|
|
||||||
if (j < i) continue
|
fun nearestPoint(query: Vector3D): Vector3D = points[nearest(query)]
|
||||||
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)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,6 +64,6 @@ class DelaunayTriangulation(val points: List<Vector2>) {
|
|||||||
*
|
*
|
||||||
* @return A DelaunayTriangulation object representing the triangulation of the given points.
|
* @return A DelaunayTriangulation object representing the triangulation of the given points.
|
||||||
*/
|
*/
|
||||||
fun List<Vector2>.delaunayTriangulation(): DelaunayTriangulation {
|
fun List<Vector3D>.delaunayTriangulation(): DelaunayTriangulation {
|
||||||
return DelaunayTriangulation(this)
|
return DelaunayTriangulation(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.openrndr.extra.triangulation
|
package com.icegps.triangulation
|
||||||
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
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.
|
// (ax,ay) (bx,by) are swapped such that the sign of the determinant is flipped. which is what Delaunator.kt expects.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.icegps.triangulation
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author tabidachinokaze
|
|
||||||
* @date 2025/11/24
|
|
||||||
*/
|
|
||||||
typealias Vector2 = Vector
|
|
||||||
@@ -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(x: Int, y: Int): Double = this.distanceTo(x.toDouble(), y.toDouble())
|
||||||
fun distanceTo(that: Vector2D): Double = distanceTo(that.x, that.y)
|
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 cross(that: Vector2D): Double = crossProduct(this, that)
|
||||||
infix fun dot(that: Vector2D): Double = ((this.x * that.x) + (this.y * that.y))
|
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) */
|
/** DOWN using screen coordinates as reference (0, +1) */
|
||||||
val DOWN_SCREEN = Vector2D(0.0, +1.0)
|
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: Number, y: Number): Vector2D = Vector2D(x.toDouble(), y.toDouble())
|
||||||
//inline operator fun invoke(x: Float, y: Float): Vector2D = Vector2D(x.toDouble(), y.toDouble())
|
//inline operator fun invoke(x: Float, y: Float): Vector2D = Vector2D(x.toDouble(), y.toDouble())
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import org.gradle.internal.os.OperatingSystem
|
|
||||||
|
|
||||||
rootProject.name = "orx"
|
rootProject.name = "orx"
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ dependencyResolutionManagement {
|
|||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
// We use a regex to get the openrndr version from the primary catalog as there is no public Gradle API to parse catalogs.
|
// 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 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") {
|
create("sharedLibs") {
|
||||||
from("org.openrndr:openrndr-dependency-catalog:$openrndrVersion")
|
from("org.openrndr:openrndr-dependency-catalog:$openrndrVersion")
|
||||||
}
|
}
|
||||||
@@ -130,4 +128,5 @@ include(":android")
|
|||||||
include(":math")
|
include(":math")
|
||||||
include(":desktop")
|
include(":desktop")
|
||||||
include(":icegps-common")
|
include(":icegps-common")
|
||||||
include(":icegps-shared")
|
include(":icegps-shared")
|
||||||
|
include(":icegps-triangulation")
|
||||||
Reference in New Issue
Block a user