Compare commits
12 Commits
f81eee8716
...
terrain
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c90073363 | |||
| 2525d30c80 | |||
| 0d15c60606 | |||
| ac86ab3976 | |||
| 816e954ed8 | |||
| de15029b2b | |||
| a1a9a9e0e4 | |||
|
|
3ba0395c16 | ||
|
|
10888b0e83 | ||
|
|
6024e62af0 | ||
|
|
4af2ed3fed | ||
|
|
522627ca51 |
@@ -51,10 +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"))
|
|
||||||
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(":icegps-triangulation"))
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.ext.junit)
|
androidTestImplementation(libs.ext.junit)
|
||||||
|
|||||||
427
android/src/main/java/com/icegps/orx/ContoursManager.kt
Normal file
427
android/src/main/java/com/icegps/orx/ContoursManager.kt
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import ColorBrewer2Type
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import colorBrewer2Palettes
|
||||||
|
import com.icegps.math.geometry.Rectangle
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.orx.catmullrom.CatmullRomChain2
|
||||||
|
import com.icegps.orx.ktx.area
|
||||||
|
import com.icegps.orx.ktx.toColorInt
|
||||||
|
import com.icegps.orx.ktx.toMapboxPoint
|
||||||
|
import com.icegps.orx.ktx.toast
|
||||||
|
import com.icegps.orx.marchingsquares.ShapeContour
|
||||||
|
import com.icegps.orx.marchingsquares.findContours
|
||||||
|
import com.icegps.shared.ktx.TAG
|
||||||
|
import com.icegps.triangulation.DelaunayTriangulation
|
||||||
|
import com.icegps.triangulation.Triangle
|
||||||
|
import com.mapbox.geojson.Feature
|
||||||
|
import com.mapbox.geojson.FeatureCollection
|
||||||
|
import com.mapbox.geojson.LineString
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class ContoursManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val mapView: MapView,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
private val sourceId: String = "contours-source-id-10"
|
||||||
|
private val layerId: String = "contours-layer-id-10"
|
||||||
|
private val fillSourceId: String = "contours-fill-source-id-10"
|
||||||
|
private val fillLayerId: String = "contours-fill-layer-id-10"
|
||||||
|
private val gridSourceId: String = "grid-polygon-source-id"
|
||||||
|
private val gridLayerId: String = "grid-polygon-layer-id"
|
||||||
|
|
||||||
|
private var contourSize: Int = 6
|
||||||
|
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
|
||||||
|
private var cellSize: Double? = 10.0
|
||||||
|
val simplePalette = SimplePalette(
|
||||||
|
range = 0.0..100.0
|
||||||
|
)
|
||||||
|
|
||||||
|
private var colors = colorBrewer2Palettes(
|
||||||
|
numberOfColors = contourSize,
|
||||||
|
paletteType = ColorBrewer2Type.Any
|
||||||
|
).first().colors.reversed()
|
||||||
|
|
||||||
|
private var points: List<Vector3D> = emptyList()
|
||||||
|
|
||||||
|
private val polylineManager = PolylineManager(mapView)
|
||||||
|
|
||||||
|
fun updateContourSize(contourSize: Int) {
|
||||||
|
this.contourSize = contourSize
|
||||||
|
colors = colorBrewer2Palettes(
|
||||||
|
numberOfColors = contourSize,
|
||||||
|
paletteType = ColorBrewer2Type.Any
|
||||||
|
).first().colors.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCellSize(value: Double) {
|
||||||
|
cellSize = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePoints(
|
||||||
|
points: List<Vector3D>,
|
||||||
|
) {
|
||||||
|
this.points = points
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHeightRange(
|
||||||
|
heightRange: ClosedFloatingPointRange<Double>? = null
|
||||||
|
) {
|
||||||
|
if (heightRange == null) {
|
||||||
|
if (points.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val height = points.map { it.z }
|
||||||
|
val range = height.min()..height.max()
|
||||||
|
this.heightRange = range
|
||||||
|
simplePalette.setRange(range)
|
||||||
|
} else {
|
||||||
|
this.heightRange = heightRange
|
||||||
|
simplePalette.setRange(heightRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isGridVisible: Boolean = true
|
||||||
|
private var _gridModel = MutableStateFlow<GridModel?>(null)
|
||||||
|
val gridModel = _gridModel.asStateFlow()
|
||||||
|
|
||||||
|
fun setGridVisible(visible: Boolean) {
|
||||||
|
if (visible != isGridVisible) {
|
||||||
|
isGridVisible = visible
|
||||||
|
if (visible) {
|
||||||
|
_gridModel.value?.let { gridModel ->
|
||||||
|
mapView.displayGridModel(
|
||||||
|
grid = gridModel,
|
||||||
|
sourceId = gridSourceId,
|
||||||
|
layerId = gridLayerId,
|
||||||
|
palette = simplePalette::palette
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(gridLayerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.styleSourceExists(gridSourceId)) {
|
||||||
|
style.removeStyleSource(gridSourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var triangles: List<Triangle> = listOf()
|
||||||
|
private var isTriangleVisible: Boolean = true
|
||||||
|
|
||||||
|
fun setTriangleVisible(visible: Boolean) {
|
||||||
|
if (visible != isTriangleVisible) {
|
||||||
|
isTriangleVisible = visible
|
||||||
|
if (visible) {
|
||||||
|
polylineManager.update(
|
||||||
|
triangles.map {
|
||||||
|
listOf(it.x1, it.x2, it.x3)
|
||||||
|
.map { Vector3D(it.x, it.y, it.z) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
polylineManager.clearContours()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
val points = points
|
||||||
|
if (points.size <= 3) {
|
||||||
|
context.toast("points size ${points.size}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job?.cancel()
|
||||||
|
scope.launch {
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
val step = heightRange.endInclusive / contourSize
|
||||||
|
val zip = (0..contourSize).map { index ->
|
||||||
|
heightRange.start + index * step
|
||||||
|
}.zipWithNext { a, b -> a..b }
|
||||||
|
val area = points.area
|
||||||
|
val triangulation = DelaunayTriangulation(points)
|
||||||
|
val triangles = triangulation.triangles()
|
||||||
|
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
|
||||||
|
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
|
||||||
|
} else {
|
||||||
|
cellSize!!
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
val gridModel = triangulationToGrid(
|
||||||
|
delaunator = triangulation,
|
||||||
|
cellSize = cellSize,
|
||||||
|
)
|
||||||
|
this@ContoursManager._gridModel.value = gridModel
|
||||||
|
if (isGridVisible) mapView.displayGridModel(
|
||||||
|
grid = gridModel,
|
||||||
|
sourceId = gridSourceId,
|
||||||
|
layerId = gridLayerId,
|
||||||
|
palette = simplePalette::palette
|
||||||
|
)
|
||||||
|
}
|
||||||
|
job = scope.launch(Dispatchers.Default) {
|
||||||
|
val lineFeatures = mutableListOf<List<Feature>>()
|
||||||
|
val features = zip.mapIndexed { index, range ->
|
||||||
|
async {
|
||||||
|
val contours = findContours(
|
||||||
|
triangles = triangles,
|
||||||
|
range = range,
|
||||||
|
area = area,
|
||||||
|
cellSize = cellSize
|
||||||
|
)
|
||||||
|
val color = colors[index].toColorInt()
|
||||||
|
lineFeatures.add(contoursToLineFeatures(contours, color).flatten())
|
||||||
|
contoursToPolygonFeatures(contours, color)
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (false) setupLineLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = sourceId,
|
||||||
|
layerId = layerId,
|
||||||
|
features = lineFeatures.flatten()
|
||||||
|
)
|
||||||
|
setupFillLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = fillSourceId,
|
||||||
|
layerId = fillLayerId,
|
||||||
|
features = features.filterNotNull(),
|
||||||
|
)
|
||||||
|
Log.d(TAG, "refresh: 刷新完成")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findContours(
|
||||||
|
triangles: List<Triangle>,
|
||||||
|
range: ClosedFloatingPointRange<Double>,
|
||||||
|
area: Rectangle,
|
||||||
|
cellSize: Double
|
||||||
|
): List<ShapeContour> {
|
||||||
|
return findContours(
|
||||||
|
f = { v ->
|
||||||
|
val triangle = triangles.firstOrNull { triangle ->
|
||||||
|
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||||
|
}
|
||||||
|
(triangle?.let { triangle ->
|
||||||
|
val interpolate = interpolateHeight(
|
||||||
|
point = v,
|
||||||
|
triangle = listOf(
|
||||||
|
triangle.x1,
|
||||||
|
triangle.x2,
|
||||||
|
triangle.x3,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (interpolate.z in range) -1.0
|
||||||
|
else 1.0
|
||||||
|
} ?: 1.0).also {
|
||||||
|
Log.d(TAG, "findContours: ${v} -> ${it}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
area = area,
|
||||||
|
cellSize = cellSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLineLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
val layer = lineLayer(layerId, sourceId) {
|
||||||
|
lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
|
||||||
|
lineWidth(1.0)
|
||||||
|
lineCap(LineCap.ROUND)
|
||||||
|
lineJoin(LineJoin.ROUND)
|
||||||
|
lineOpacity(0.8)
|
||||||
|
}
|
||||||
|
style.addLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFillLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
val layer = fillLayer(layerId, sourceId) {
|
||||||
|
fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||||
|
fillOpacity(0.5)
|
||||||
|
fillAntialias(true)
|
||||||
|
}
|
||||||
|
style.addLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var useCatmullRom: Boolean = true
|
||||||
|
|
||||||
|
fun setCatmullRom(enabled: Boolean) {
|
||||||
|
useCatmullRom = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contoursToLineFeatures(contours: List<ShapeContour>, color: Int): List<List<Feature>> {
|
||||||
|
return contours.drop(1).map { contour ->
|
||||||
|
contour.segments.map { segment ->
|
||||||
|
LineString.fromLngLats(
|
||||||
|
listOf(
|
||||||
|
segment.start.toMapboxPoint(),
|
||||||
|
segment.end.toMapboxPoint()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.map { lineString ->
|
||||||
|
Feature.fromGeometry(lineString).apply {
|
||||||
|
// 将颜色Int转换为十六进制字符串
|
||||||
|
addStringProperty("color", color.toHexColorString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contoursToPolygonFeatures(contours: List<ShapeContour>, color: Int): Feature? {
|
||||||
|
val lists = contours.drop(0).filter { it.segments.isNotEmpty() }.map { contour ->
|
||||||
|
val start = contour.segments[0].start
|
||||||
|
listOf(start) + contour.segments.map { it.end }
|
||||||
|
}.map {
|
||||||
|
if (!useCatmullRom) return@map it
|
||||||
|
val cmr = CatmullRomChain2(it, 1.0, loop = true)
|
||||||
|
val contour = ShapeContour.fromPoints(cmr.positions(200), true)
|
||||||
|
val start = contour.segments[0].start
|
||||||
|
listOf(start) + contour.segments.map { it.end }
|
||||||
|
}.map { points -> points.map { it.toMapboxPoint() } }
|
||||||
|
|
||||||
|
if (lists.isEmpty()) {
|
||||||
|
Log.w(TAG, "contoursToPolygonFeatures: 没有有效的轮廓数据")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val polygon = Polygon.fromLngLats(lists)
|
||||||
|
return Feature.fromGeometry(polygon).apply {
|
||||||
|
// 将颜色Int转换为十六进制字符串
|
||||||
|
addStringProperty("color", color.toHexColorString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.toHexColorString(): String {
|
||||||
|
return String.format("#%06X", 0xFFFFFF and this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearContours() {
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPointInTriangle3D(point: Vector2D, triangle: List<Vector3D>): Boolean {
|
||||||
|
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||||
|
|
||||||
|
val (v1, v2, v3) = triangle
|
||||||
|
|
||||||
|
// 计算重心坐标
|
||||||
|
val denominator = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||||
|
if (denominator == 0.0) return false // 退化三角形
|
||||||
|
|
||||||
|
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denominator
|
||||||
|
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denominator
|
||||||
|
val gamma = 1.0 - alpha - beta
|
||||||
|
|
||||||
|
// 点在三角形内当且仅当所有重心坐标都在[0,1]范围内
|
||||||
|
return alpha >= 0 && beta >= 0 && gamma >= 0 &&
|
||||||
|
alpha <= 1 && beta <= 1 && gamma <= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用重心坐标计算点在三角形上的高度
|
||||||
|
*
|
||||||
|
* @param point 二维点 (x, y)
|
||||||
|
* @param triangle 三角形的三个顶点
|
||||||
|
* @return 三维点 (x, y, z)
|
||||||
|
*/
|
||||||
|
fun interpolateHeight(point: Vector2D, triangle: List<Vector3D>): Vector3D {
|
||||||
|
/**
|
||||||
|
* 计算点在三角形中的重心坐标
|
||||||
|
*/
|
||||||
|
fun calculateBarycentricCoordinates(
|
||||||
|
point: Vector2D,
|
||||||
|
v1: Vector3D,
|
||||||
|
v2: Vector3D,
|
||||||
|
v3: Vector3D
|
||||||
|
): Triple<Double, Double, Double> {
|
||||||
|
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||||
|
|
||||||
|
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denom
|
||||||
|
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denom
|
||||||
|
val gamma = 1.0 - alpha - beta
|
||||||
|
|
||||||
|
return Triple(alpha, beta, gamma)
|
||||||
|
}
|
||||||
|
|
||||||
|
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||||
|
|
||||||
|
val (v1, v2, v3) = triangle
|
||||||
|
|
||||||
|
// 计算重心坐标
|
||||||
|
val (alpha, beta, gamma) = calculateBarycentricCoordinates(point, v1, v2, v3)
|
||||||
|
|
||||||
|
// 使用重心坐标插值z值
|
||||||
|
val z = alpha * v1.z + beta * v2.z + gamma * v3.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
android/src/main/java/com/icegps/orx/CoordinateGenerator.kt
Normal file
53
android/src/main/java/com/icegps/orx/CoordinateGenerator.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Angle
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.math.geometry.degrees
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
fun coordinateGenerate(): List<Vector3D> {
|
||||||
|
val minX = -20.0
|
||||||
|
val maxX = 20.0
|
||||||
|
val minY = -20.0
|
||||||
|
val maxY = 20.0
|
||||||
|
val minZ = -20.0
|
||||||
|
val maxZ = 20.0
|
||||||
|
val x: () -> Double = { Random.nextDouble(minX, maxX) }
|
||||||
|
val y: () -> Double = { Random.nextDouble(minY, maxY) }
|
||||||
|
val z: () -> Double = { Random.nextDouble(minZ, maxZ) }
|
||||||
|
val dPoints = (0..60).map {
|
||||||
|
Vector3D(x(), y(), z())
|
||||||
|
}
|
||||||
|
return dPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
fun coordinateGenerate1(): List<List<Vector3D>> {
|
||||||
|
/**
|
||||||
|
* 绕 Z 轴旋转指定角度(弧度)
|
||||||
|
*/
|
||||||
|
fun Vector3D.rotateAroundZ(angle: Angle): Vector3D {
|
||||||
|
val cosAngle = cos(angle.radians)
|
||||||
|
val sinAngle = sin(angle.radians)
|
||||||
|
|
||||||
|
return Vector3D(
|
||||||
|
x = x * cosAngle - y * sinAngle,
|
||||||
|
y = x * sinAngle + y * cosAngle,
|
||||||
|
z = z
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val center = Vector3D()
|
||||||
|
val direction = Vector3D(0.0, 1.0, -1.0)
|
||||||
|
return (0..360).step(10).map {
|
||||||
|
val nowDirection = direction.rotateAroundZ(it.degrees)
|
||||||
|
listOf(2, 6, 10).map {
|
||||||
|
center + nowDirection * it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
83
android/src/main/java/com/icegps/orx/GridDisplay.kt
Normal file
83
android/src/main/java/com/icegps/orx/GridDisplay.kt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.mapbox.geojson.Feature
|
||||||
|
import com.mapbox.geojson.FeatureCollection
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
import com.mapbox.geojson.Polygon
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
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.sources.addSource
|
||||||
|
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
fun MapView.displayGridModel(
|
||||||
|
grid: GridModel,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
palette: (Double?) -> String,
|
||||||
|
) {
|
||||||
|
val geoHelper = GeoHelper.getSharedInstance()
|
||||||
|
mapboxMap.getStyle { style ->
|
||||||
|
val polygonFeatures = mutableListOf<Feature>()
|
||||||
|
|
||||||
|
val minX = grid.minX
|
||||||
|
val maxY = grid.maxY
|
||||||
|
val cellSize = grid.cellSize
|
||||||
|
|
||||||
|
for (r in 0 until grid.rows) {
|
||||||
|
for (c in 0 until grid.cols) {
|
||||||
|
val idx = r * grid.cols + c
|
||||||
|
val v = grid.cells[idx] ?: continue
|
||||||
|
|
||||||
|
val x0 = minX + c * cellSize
|
||||||
|
val y0 = maxY - r * cellSize
|
||||||
|
val x1 = x0 + cellSize
|
||||||
|
val y1 = y0 - cellSize
|
||||||
|
|
||||||
|
val ring = listOf(
|
||||||
|
Vector2D(x0, y0),
|
||||||
|
Vector2D(x1, y0),
|
||||||
|
Vector2D(x1, y1),
|
||||||
|
Vector2D(x0, y1),
|
||||||
|
Vector2D(x0, y0),
|
||||||
|
).map {
|
||||||
|
geoHelper.enuToWGS84Object(GeoHelper.ENU(it.x, it.y))
|
||||||
|
}.map {
|
||||||
|
Point.fromLngLat(it.lon, it.lat)
|
||||||
|
}
|
||||||
|
val poly = Polygon.fromLngLats(listOf(ring))
|
||||||
|
val polyFeature = Feature.fromGeometry(poly)
|
||||||
|
polyFeature.addStringProperty("color", palette(v))
|
||||||
|
polyFeature.addNumberProperty("value", v ?: -9999.0)
|
||||||
|
polygonFeatures.add(polyFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.styleSourceExists(sourceId)) {
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val polygonSource = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(polygonFeatures))
|
||||||
|
}
|
||||||
|
style.addSource(polygonSource)
|
||||||
|
|
||||||
|
val fillLayer = FillLayer(layerId, sourceId).apply {
|
||||||
|
fillColor(Expression.toColor(Expression.get("color")))
|
||||||
|
fillOpacity(0.5)
|
||||||
|
}
|
||||||
|
style.addLayer(fillLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
139
android/src/main/java/com/icegps/orx/GridModel.kt
Normal file
139
android/src/main/java/com/icegps/orx/GridModel.kt
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.triangulation.DelaunayTriangulation
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
data class GridModel(
|
||||||
|
val minX: Double,
|
||||||
|
val maxX: Double,
|
||||||
|
val minY: Double,
|
||||||
|
val maxY: Double,
|
||||||
|
val rows: Int,
|
||||||
|
val cols: Int,
|
||||||
|
val cellSize: 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(
|
||||||
|
delaunator: DelaunayTriangulation,
|
||||||
|
cellSize: Double = 50.0,
|
||||||
|
maxSidePixels: Int = 5000
|
||||||
|
): GridModel {
|
||||||
|
fun pointInTriangle(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D): Boolean {
|
||||||
|
val v0x = c.x - a.x
|
||||||
|
val v0y = c.y - a.y
|
||||||
|
val v1x = b.x - a.x
|
||||||
|
val v1y = b.y - a.y
|
||||||
|
val v2x = pt.x - a.x
|
||||||
|
val v2y = pt.y - a.y
|
||||||
|
|
||||||
|
val dot00 = v0x * v0x + v0y * v0y
|
||||||
|
val dot01 = v0x * v1x + v0y * v1y
|
||||||
|
val dot02 = v0x * v2x + v0y * v2y
|
||||||
|
val dot11 = v1x * v1x + v1y * v1y
|
||||||
|
val dot12 = v1x * v2x + v1y * v2y
|
||||||
|
|
||||||
|
val denom = dot00 * dot11 - dot01 * dot01
|
||||||
|
if (denom == 0.0) return false
|
||||||
|
val invDenom = 1.0 / denom
|
||||||
|
val u = (dot11 * dot02 - dot01 * dot12) * invDenom
|
||||||
|
val v = (dot00 * dot12 - dot01 * dot02) * invDenom
|
||||||
|
return u >= 0 && v >= 0 && u + v <= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D, values: DoubleArray): Double {
|
||||||
|
val area = { p1: Vector2D, p2: Vector3D, p3: Vector3D ->
|
||||||
|
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
|
||||||
|
}
|
||||||
|
val area2 = { p1: Vector3D, p2: Vector3D, p3: Vector3D ->
|
||||||
|
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
|
||||||
|
}
|
||||||
|
val areaTotal = area2(a, b, c)
|
||||||
|
if (areaTotal == 0.0) return values[0]
|
||||||
|
val wA = area(pt, b, c) / areaTotal
|
||||||
|
val wB = area(pt, c, a) / areaTotal
|
||||||
|
val wC = area(pt, a, b) / areaTotal
|
||||||
|
return values[0] * wA + values[1] * wB + values[2] * wC
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val pts = delaunator.points
|
||||||
|
require(pts.isNotEmpty()) { "points empty" }
|
||||||
|
|
||||||
|
val x = pts.map { it.x }
|
||||||
|
val y = pts.map { it.y }
|
||||||
|
val minX = x.min()
|
||||||
|
val maxX = x.max()
|
||||||
|
val minY = y.min()
|
||||||
|
val maxY = y.max()
|
||||||
|
|
||||||
|
val width = maxX - minX
|
||||||
|
val height = maxY - minY
|
||||||
|
|
||||||
|
var cols = ceil(width / cellSize).toInt()
|
||||||
|
var rows = ceil(height / cellSize).toInt()
|
||||||
|
|
||||||
|
// 防止过大
|
||||||
|
if (cols > maxSidePixels) cols = maxSidePixels
|
||||||
|
if (rows > maxSidePixels) rows = maxSidePixels
|
||||||
|
|
||||||
|
val cells = Array<Double?>(rows * cols) { null }
|
||||||
|
|
||||||
|
|
||||||
|
val triangles = delaunator.triangles()
|
||||||
|
|
||||||
|
for (ti in 0 until triangles.size) {
|
||||||
|
val (a, b, c) = triangles[ti]
|
||||||
|
|
||||||
|
val tminX = minOf(a.x, b.x, c.x)
|
||||||
|
val tmaxX = maxOf(a.x, b.x, c.x)
|
||||||
|
val tminY = minOf(a.y, b.y, c.y)
|
||||||
|
val tmaxY = maxOf(a.y, b.y, c.y)
|
||||||
|
|
||||||
|
val colMin = ((tminX - minX) / cellSize).toInt().coerceIn(0, cols - 1)
|
||||||
|
val colMax = ((tmaxX - minX) / cellSize).toInt().coerceIn(0, cols - 1)
|
||||||
|
val rowMin = ((maxY - tmaxY) / cellSize).toInt().coerceIn(0, rows - 1)
|
||||||
|
val rowMax = ((maxY - tminY) / cellSize).toInt().coerceIn(0, rows - 1)
|
||||||
|
|
||||||
|
val triVertexVals = doubleArrayOf(a.z, b.z, c.z)
|
||||||
|
|
||||||
|
for (r in rowMin..rowMax) {
|
||||||
|
for (cIdx in colMin..colMax) {
|
||||||
|
val centerX = minX + (cIdx + 0.5) * cellSize
|
||||||
|
val centerY = maxY - (r + 0.5) * cellSize
|
||||||
|
val pt = Vector2D(centerX, centerY)
|
||||||
|
if (pointInTriangle(pt, a, b, c)) {
|
||||||
|
val idx = r * cols + cIdx
|
||||||
|
val valInterp = barycentricInterpolateLegacy(pt, a, b, c, triVertexVals)
|
||||||
|
cells[idx] = valInterp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val grid = GridModel(
|
||||||
|
minX = minX,
|
||||||
|
minY = minY,
|
||||||
|
maxX = maxX,
|
||||||
|
maxY = maxY,
|
||||||
|
rows = rows,
|
||||||
|
cols = cols,
|
||||||
|
cellSize = cellSize,
|
||||||
|
cells = cells
|
||||||
|
)
|
||||||
|
return grid
|
||||||
|
}
|
||||||
@@ -1,77 +1,220 @@
|
|||||||
package com.icegps.orx
|
package com.icegps.orx
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.slider.RangeSlider
|
||||||
|
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.SharedHttpClient
|
|
||||||
import com.icegps.shared.SharedJson
|
|
||||||
import com.icegps.shared.api.OpenElevation
|
|
||||||
import com.icegps.shared.api.OpenElevationApi
|
|
||||||
import com.icegps.shared.ktx.TAG
|
|
||||||
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.MapView
|
||||||
|
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.openrndr.extra.triangulation.DelaunayTriangulation
|
|
||||||
import org.openrndr.math.Vector2
|
|
||||||
import org.openrndr.math.Vector3
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var mapView: MapView
|
||||||
|
private val viewModel: MainViewModel by lazy {
|
||||||
|
ViewModelProvider(this)[MainViewModel::class.java]
|
||||||
|
}
|
||||||
|
private lateinit var contoursManager: ContoursManager
|
||||||
|
private lateinit var earthworkManager: EarthworkManager
|
||||||
|
|
||||||
|
init {
|
||||||
|
initGeoHelper()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
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())
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapView.mapboxMap.setCamera(
|
||||||
|
CameraOptions.Builder()
|
||||||
|
.center(Point.fromLngLat(home.longitude, home.latitude))
|
||||||
|
.pitch(0.0)
|
||||||
|
.zoom(18.0)
|
||||||
|
.bearing(0.0)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val points = coordinateGenerate()
|
||||||
|
|
||||||
|
// divider
|
||||||
|
contoursManager = ContoursManager(
|
||||||
|
context = this,
|
||||||
|
mapView = mapView,
|
||||||
|
scope = lifecycleScope
|
||||||
|
)
|
||||||
|
contoursManager.updateContourSize(6)
|
||||||
|
contoursManager.updatePoints(points)
|
||||||
|
val height = points.map { it.z }
|
||||||
|
val min = height.min()
|
||||||
|
val max = height.max()
|
||||||
|
contoursManager.updateHeightRange((min / 2)..max)
|
||||||
|
binding.heightRange.values = listOf(min.toFloat() / 2, max.toFloat())
|
||||||
|
binding.heightRange.valueFrom = min.toFloat()
|
||||||
|
binding.heightRange.valueTo = max.toFloat()
|
||||||
|
contoursManager.refresh()
|
||||||
|
|
||||||
|
binding.sliderTargetHeight.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(p0: Slider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(p0: Slider) {
|
||||||
|
val present = p0.value / p0.valueTo
|
||||||
|
// val targetHeight = ((valueRange.endInclusive - valueRange.start) * present) + valueRange.start
|
||||||
|
|
||||||
|
// val contours = findContours(triangles, targetHeight)
|
||||||
|
// contoursTest.clearContours()
|
||||||
|
// if (false) contoursTest.updateContours(contours)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.heightRange.addOnSliderTouchListener(
|
||||||
|
object : RangeSlider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(slider: RangeSlider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: RangeSlider) {
|
||||||
|
contoursManager.updateHeightRange((slider.values.min().toDouble() - 1.0)..(slider.values.max().toDouble() + 1.0))
|
||||||
|
contoursManager.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.switchGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
contoursManager.setGridVisible(isChecked)
|
||||||
|
}
|
||||||
|
binding.switchTriangle.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
contoursManager.setTriangleVisible(isChecked)
|
||||||
|
}
|
||||||
|
binding.update.setOnClickListener {
|
||||||
|
contoursManager.refresh()
|
||||||
|
}
|
||||||
|
binding.cellSize.addOnSliderTouchListener(
|
||||||
|
object : Slider.OnSliderTouchListener {
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
contoursManager.updateCellSize(slider.value.toDouble())
|
||||||
|
contoursManager.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mapView.mapboxMap.addOnMapClickListener {
|
||||||
|
viewModel.addPoint(it)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
binding.clearPoints.setOnClickListener {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val showDesignHeight = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private fun initData() {
|
||||||
|
viewModel.points.onEach {
|
||||||
|
contoursManager.updatePoints(it)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainViewModel : ViewModel() {
|
val home = GeoPoint(114.476060, 22.771073, 30.897)
|
||||||
private val geoHelper = GeoHelper.getSharedInstance()
|
|
||||||
private val openElevation: OpenElevationApi = OpenElevation(SharedHttpClient(SharedJson()))
|
|
||||||
|
|
||||||
private val _points = MutableStateFlow<List<Point>>(emptyList())
|
fun initGeoHelper(base: GeoPoint = home) {
|
||||||
|
val geoHelper = GeoHelper.getSharedInstance()
|
||||||
init {
|
geoHelper.wgs84ToENU(
|
||||||
_points.map {
|
lon = base.longitude,
|
||||||
openElevation.lookup(it.map { GeoPoint(it.longitude(), it.latitude(), it.altitude()) })
|
lat = base.latitude,
|
||||||
}.catch {
|
hgt = base.altitude
|
||||||
Log.e(TAG, "高程请求失败", it)
|
)
|
||||||
}.map {
|
}
|
||||||
it.map {
|
|
||||||
val enu =
|
|
||||||
geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude)
|
|
||||||
Vector2(enu.x, enu.y)
|
|
||||||
}
|
|
||||||
}.onEach {
|
|
||||||
val triangulation = DelaunayTriangulation(it)
|
|
||||||
triangulation.triangles().map {
|
|
||||||
it.contour
|
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addPoint(point: Point) {
|
|
||||||
_points.update {
|
|
||||||
it.toMutableList().apply {
|
|
||||||
add(point)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
59
android/src/main/java/com/icegps/orx/MainViewModel.kt
Normal file
59
android/src/main/java/com/icegps/orx/MainViewModel.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.orx.ktx.toast
|
||||||
|
import com.icegps.shared.SharedHttpClient
|
||||||
|
import com.icegps.shared.SharedJson
|
||||||
|
import com.icegps.shared.api.OpenElevation
|
||||||
|
import com.icegps.shared.api.OpenElevationApi
|
||||||
|
import com.icegps.shared.ktx.TAG
|
||||||
|
import com.icegps.shared.model.GeoPoint
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
class MainViewModel(private val context: Application) : AndroidViewModel(context) {
|
||||||
|
private val geoHelper = GeoHelper.Companion.getSharedInstance()
|
||||||
|
private val openElevation: OpenElevationApi = OpenElevation(SharedHttpClient(SharedJson()))
|
||||||
|
|
||||||
|
private val _points = MutableStateFlow<List<Point>>(emptyList())
|
||||||
|
val points = _points.filter { it.size > 3 }.debounce(1000).map {
|
||||||
|
openElevation.lookup(it.map { GeoPoint(it.longitude(), it.latitude(), it.altitude()) })
|
||||||
|
}.catch {
|
||||||
|
Log.e(TAG, "高程请求失败", it)
|
||||||
|
context.toast("高程请求失败")
|
||||||
|
}.map {
|
||||||
|
it.map {
|
||||||
|
val enu = geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude)
|
||||||
|
Vector3D(enu.x, enu.y, enu.z)
|
||||||
|
}
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.Companion.Eagerly,
|
||||||
|
initialValue = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun addPoint(point: Point) {
|
||||||
|
context.toast("${point.longitude()}, ${point.latitude()}")
|
||||||
|
_points.update {
|
||||||
|
it.toMutableList().apply {
|
||||||
|
add(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearPoints() {
|
||||||
|
_points.value = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
123
android/src/main/java/com/icegps/orx/PolygonTest.kt
Normal file
123
android/src/main/java/com/icegps/orx/PolygonTest.kt
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
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.Polygon
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.Style
|
||||||
|
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
|
||||||
|
|
||||||
|
class PolygonTest(
|
||||||
|
private val mapView: MapView
|
||||||
|
) {
|
||||||
|
private val geoHelper = GeoHelper.Companion.getSharedInstance()
|
||||||
|
|
||||||
|
private val contourSourceId = "contour-source-id-0"
|
||||||
|
private val contourLayerId = "contour-layer-id-0"
|
||||||
|
|
||||||
|
private val fillSourceId = "fill-source-id-0"
|
||||||
|
private val fillLayerId = "fill-layer-id-0"
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
outer: List<Vector3D>,
|
||||||
|
inner: List<Vector3D>,
|
||||||
|
other: List<Vector3D>
|
||||||
|
) {
|
||||||
|
val lineFeatures = mutableListOf<Feature>()
|
||||||
|
val fillFeatures = mutableListOf<Feature>()
|
||||||
|
|
||||||
|
val outerPoints = outer.map { it.toMapboxPoint() }
|
||||||
|
val innerPoints = inner.map { it.toMapboxPoint() }
|
||||||
|
val otherPoints = other.map { it.toMapboxPoint() }
|
||||||
|
val outerLine = LineString.fromLngLats(outerPoints)
|
||||||
|
Feature.fromGeometry(outerLine).also {
|
||||||
|
lineFeatures.add(it)
|
||||||
|
}
|
||||||
|
val innerLine = LineString.fromLngLats(innerPoints)
|
||||||
|
Feature.fromGeometry(innerLine).also {
|
||||||
|
lineFeatures.add(it)
|
||||||
|
}
|
||||||
|
Feature.fromGeometry(LineString.fromLngLats(otherPoints)).also {
|
||||||
|
lineFeatures.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
//val polygon = Polygon.fromOuterInner(outerLine, innerLine)
|
||||||
|
val polygon = Polygon.fromLngLats(listOf(outerPoints, otherPoints, innerPoints))
|
||||||
|
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
if (false) setupLineLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = contourSourceId,
|
||||||
|
layerId = contourLayerId,
|
||||||
|
features = lineFeatures
|
||||||
|
)
|
||||||
|
setupFillLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = fillSourceId,
|
||||||
|
layerId = fillLayerId,
|
||||||
|
features = listOf(Feature.fromGeometry(polygon))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLineLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
val layer = lineLayer(layerId, sourceId) {
|
||||||
|
lineColor(Color.RED)
|
||||||
|
lineWidth(2.0)
|
||||||
|
lineCap(LineCap.Companion.ROUND)
|
||||||
|
lineJoin(LineJoin.Companion.ROUND)
|
||||||
|
lineOpacity(0.8)
|
||||||
|
}
|
||||||
|
style.addLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFillLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
val layer = fillLayer(fillLayerId, fillSourceId) {
|
||||||
|
fillColor(Color.YELLOW)
|
||||||
|
fillOpacity(0.3)
|
||||||
|
fillAntialias(true)
|
||||||
|
}
|
||||||
|
style.addLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
121
android/src/main/java/com/icegps/orx/PolylineManager.kt
Normal file
121
android/src/main/java/com/icegps/orx/PolylineManager.kt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.icegps.math.geometry.Line3D
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.orx.ktx.toMapboxPoint
|
||||||
|
import com.mapbox.geojson.Feature
|
||||||
|
import com.mapbox.geojson.FeatureCollection
|
||||||
|
import com.mapbox.geojson.LineString
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.Style
|
||||||
|
import com.mapbox.maps.extension.style.layers.addLayer
|
||||||
|
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
|
||||||
|
|
||||||
|
class PolylineManager(
|
||||||
|
private val mapView: MapView
|
||||||
|
) {
|
||||||
|
private val sourceId: String = "polyline-source-id-0"
|
||||||
|
private val layerId: String = "polyline-layer-id-0"
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
points: List<List<Vector3D>>
|
||||||
|
) {
|
||||||
|
val lineStrings: List<List<Feature>> = points.map {
|
||||||
|
val lines = fromPoints(it, true)
|
||||||
|
lines.map {
|
||||||
|
LineString.fromLngLats(listOf(it.a.toMapboxPoint(), it.b.toMapboxPoint()))
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
it.map { Feature.fromGeometry(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
setupLineLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = sourceId,
|
||||||
|
layerId = layerId,
|
||||||
|
features = lineStrings.flatten()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFeatures(
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
setupLineLayer(
|
||||||
|
style = style,
|
||||||
|
sourceId = sourceId,
|
||||||
|
layerId = layerId,
|
||||||
|
features = features
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLineLayer(
|
||||||
|
style: Style,
|
||||||
|
sourceId: String,
|
||||||
|
layerId: String,
|
||||||
|
features: List<Feature>
|
||||||
|
) {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
|
||||||
|
val source = geoJsonSource(sourceId) {
|
||||||
|
featureCollection(FeatureCollection.fromFeatures(features))
|
||||||
|
}
|
||||||
|
style.addSource(source)
|
||||||
|
|
||||||
|
val layer = lineLayer(layerId, sourceId) {
|
||||||
|
lineColor(Color.RED)
|
||||||
|
lineWidth(2.0)
|
||||||
|
lineCap(LineCap.Companion.ROUND)
|
||||||
|
lineJoin(LineJoin.Companion.ROUND)
|
||||||
|
lineOpacity(0.8)
|
||||||
|
}
|
||||||
|
style.addLayer(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearContours() {
|
||||||
|
mapView.mapboxMap.getStyle { style ->
|
||||||
|
try {
|
||||||
|
style.removeStyleLayer(layerId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
style.removeStyleSource(sourceId)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromPoints(
|
||||||
|
points: List<Vector3D>,
|
||||||
|
closed: Boolean,
|
||||||
|
) = if (points.isEmpty()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
if (!closed) {
|
||||||
|
(0 until points.size - 1).map {
|
||||||
|
Line3D(
|
||||||
|
points[it],
|
||||||
|
points[it + 1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val d = (points.last() - points.first()).length
|
||||||
|
val usePoints = if (d > 1E-6) points else points.dropLast(1)
|
||||||
|
(usePoints.indices).map {
|
||||||
|
Line3D(
|
||||||
|
usePoints[it],
|
||||||
|
usePoints[(it + 1) % usePoints.size]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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() })
|
||||||
|
}
|
||||||
|
}
|
||||||
123
android/src/main/java/com/icegps/orx/SimplePalette.kt
Normal file
123
android/src/main/java/com/icegps/orx/SimplePalette.kt
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package com.icegps.orx
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
class SimplePalette(
|
||||||
|
private var range: ClosedFloatingPointRange<Double>
|
||||||
|
) {
|
||||||
|
fun setRange(range: ClosedFloatingPointRange<Double>) {
|
||||||
|
this.range = range
|
||||||
|
}
|
||||||
|
|
||||||
|
private val colors: Map<Int, String>
|
||||||
|
|
||||||
|
init {
|
||||||
|
colors = generateTerrainColorMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun palette(value: Double?): String {
|
||||||
|
if (value == null) return "#00000000"
|
||||||
|
val minH = range.start
|
||||||
|
val maxH = range.endInclusive
|
||||||
|
val normalized = ((value - minH) / (maxH - minH)).coerceIn(0.0, 1.0)
|
||||||
|
return colors[(normalized * 255).toInt()] ?: "#00000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun palette1(value: Double?): String {
|
||||||
|
return if (value == null) "#00000000" else {
|
||||||
|
// 假设您已经知道高度范围,或者动态计算
|
||||||
|
val minH = range.start
|
||||||
|
val maxH = range.endInclusive
|
||||||
|
val normalized = ((value - minH) / (maxH - minH)).coerceIn(0.0, 1.0)
|
||||||
|
val alpha = (normalized * 255).toInt()
|
||||||
|
String.format("#%02X%02X%02X", alpha, 0, 0)
|
||||||
|
}.also {
|
||||||
|
Log.d("simplePalette", "$value -> $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateGrayscaleColorMap2(): MutableMap<Int, String> {
|
||||||
|
val colorMap = mutableMapOf<Int, String>()
|
||||||
|
|
||||||
|
// 定义关键灰度点
|
||||||
|
val black = Color(0, 0, 0) // 低地势 - 黑色
|
||||||
|
val darkGray = Color(64, 64, 64) // 过渡
|
||||||
|
val midGray = Color(128, 128, 128) // 中间
|
||||||
|
val lightGray = Color(192, 192, 192) // 过渡
|
||||||
|
val white = Color(255, 255, 255) // 高地势 - 白色
|
||||||
|
|
||||||
|
for (i in 0..255) {
|
||||||
|
val position = i / 255.0
|
||||||
|
|
||||||
|
val color = when {
|
||||||
|
position < 0.25 -> interpolateColor(black, darkGray, position / 0.25)
|
||||||
|
position < 0.5 -> interpolateColor(darkGray, midGray, (position - 0.25) / 0.25)
|
||||||
|
position < 0.75 -> interpolateColor(midGray, lightGray, (position - 0.5) / 0.25)
|
||||||
|
else -> interpolateColor(lightGray, white, (position - 0.75) / 0.25)
|
||||||
|
}
|
||||||
|
colorMap[i] = color.toHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateGrayscaleColorMap(): MutableMap<Int, String> {
|
||||||
|
val colorMap = mutableMapOf<Int, String>()
|
||||||
|
|
||||||
|
for (i in 0..255) {
|
||||||
|
// 从黑色到白色的线性渐变
|
||||||
|
val grayValue = i
|
||||||
|
val color = Color(grayValue, grayValue, grayValue)
|
||||||
|
colorMap[i] = color.toHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateTerrainColorMap(): MutableMap<Int, String> {
|
||||||
|
val colorMap = mutableMapOf<Int, String>()
|
||||||
|
|
||||||
|
// 定义关键颜色点
|
||||||
|
val blue = Color(0, 0, 255) // 低地势 - 蓝色
|
||||||
|
val cyan = Color(0, 255, 255) // 中间过渡
|
||||||
|
val green = Color(0, 255, 0) // 中间过渡
|
||||||
|
val yellow = Color(255, 255, 0) // 中间过渡
|
||||||
|
val red = Color(255, 0, 0) // 高地势 - 红色
|
||||||
|
|
||||||
|
for (i in 0..255) {
|
||||||
|
val position = i / 255.0
|
||||||
|
|
||||||
|
val color = when {
|
||||||
|
position < 0.25 -> interpolateColor(blue, cyan, position / 0.25)
|
||||||
|
position < 0.5 -> interpolateColor(cyan, green, (position - 0.25) / 0.25)
|
||||||
|
position < 0.75 -> interpolateColor(green, yellow, (position - 0.5) / 0.25)
|
||||||
|
else -> interpolateColor(yellow, red, (position - 0.75) / 0.25)
|
||||||
|
}
|
||||||
|
colorMap[i] = color.toHex()
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interpolateColor(start: Color, end: Color, fraction: Double): Color {
|
||||||
|
val r = (start.red + (end.red - start.red) * fraction).toInt()
|
||||||
|
val g = (start.green + (end.green - start.green) * fraction).toInt()
|
||||||
|
val b = (start.blue + (end.blue - start.blue) * fraction).toInt()
|
||||||
|
return Color(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color类简化实现
|
||||||
|
class Color(val red: Int, val green: Int, val blue: Int) {
|
||||||
|
fun toArgb(): Int {
|
||||||
|
return (0xFF shl 24) or (red shl 16) or (green shl 8) or blue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toHex(): String {
|
||||||
|
return String.format("#%06X", toArgb() and 0xFFFFFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
23
android/src/main/java/com/icegps/orx/ktx/ColorRGBa.kt
Normal file
23
android/src/main/java/com/icegps/orx/ktx/ColorRGBa.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.icegps.orx.ktx
|
||||||
|
|
||||||
|
import com.icegps.orx.color.ColorRGBa
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
fun ColorRGBa.toColorInt(): Int {
|
||||||
|
val clampedR = r.coerceIn(0.0, 1.0)
|
||||||
|
val clampedG = g.coerceIn(0.0, 1.0)
|
||||||
|
val clampedB = b.coerceIn(0.0, 1.0)
|
||||||
|
val clampedAlpha = alpha.coerceIn(0.0, 1.0)
|
||||||
|
|
||||||
|
return ((clampedAlpha * 255).toInt() shl 24) or
|
||||||
|
((clampedR * 255).toInt() shl 16) or
|
||||||
|
((clampedG * 255).toInt() shl 8) or
|
||||||
|
((clampedB * 255).toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ColorRGBa.toColorHex(): String {
|
||||||
|
return String.format("#%06X", 0xFFFFFF and toColorInt())
|
||||||
|
}
|
||||||
12
android/src/main/java/com/icegps/orx/ktx/Context.kt
Normal file
12
android/src/main/java/com/icegps/orx/ktx/Context.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.icegps.orx.ktx
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tabidachinokaze
|
||||||
|
* @date 2025/11/25
|
||||||
|
*/
|
||||||
|
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
|
Toast.makeText(this, text, duration).show()
|
||||||
|
}
|
||||||
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
|
||||||
32
android/src/main/java/com/icegps/orx/ktx/Vector3D.kt
Normal file
32
android/src/main/java/com/icegps/orx/ktx/Vector3D.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package com.icegps.orx.ktx
|
||||||
|
|
||||||
|
import com.icegps.common.helper.GeoHelper
|
||||||
|
import com.icegps.math.geometry.Rectangle
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
|
||||||
|
fun Vector3D.niceStr(): String {
|
||||||
|
return "[$x, $y, $z]".format(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<Vector3D>.niceStr(): String {
|
||||||
|
return joinToString(", ", "[", "]") {
|
||||||
|
it.niceStr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Vector3D.toMapboxPoint(): Point {
|
||||||
|
val geoHelper = GeoHelper.getSharedInstance()
|
||||||
|
return geoHelper.enuToWGS84Object(GeoHelper.ENU(x, y, z)).run {
|
||||||
|
Point.fromLngLat(lon, lat, hgt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val List<Vector3D>.area: Rectangle
|
||||||
|
get() {
|
||||||
|
val minX = minOf { it.x }
|
||||||
|
val maxX = maxOf { it.x }
|
||||||
|
val minY = minOf { it.y }
|
||||||
|
val maxY = maxOf { it.y }
|
||||||
|
return 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
|
||||||
|
}
|
||||||
183
android/src/main/res/layout-port/activity_main.xml
Normal file
183
android/src/main/res/layout-port/activity_main.xml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<com.mapbox.maps.MapView
|
||||||
|
android:id="@+id/map_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="3" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slider_target_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:value="0"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
|
||||||
|
<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/cell_size"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:value="1"
|
||||||
|
android:valueFrom="1"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</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.RangeSlider
|
||||||
|
android:id="@+id/height_range"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_grid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="栅格网" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_triangle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="三角网" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坐标数量:" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/point_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/update"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="刷新界面" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/clear_points"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,19 +1,179 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:text="Hello World!"
|
android:layout_weight="1"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:orientation="vertical"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:paddingTop="32dp">
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slider_target_height"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:value="0"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
|
||||||
|
<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/cell_size"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:value="1"
|
||||||
|
android:valueFrom="1"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</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.RangeSlider
|
||||||
|
android:id="@+id/height_range"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="100" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_grid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="栅格网" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/switch_triangle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:switchPadding="16dp"
|
||||||
|
android:text="三角网" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="坐标数量:" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/point_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/update"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="刷新界面" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/clear_points"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<com.mapbox.maps.MapView
|
||||||
|
android:id="@+id/map_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -132,7 +132,7 @@ abstract class CollectScreenshotsTask @Inject constructor() : DefaultTask() {
|
|||||||
val codeLines = ktFile.readLines()
|
val codeLines = ktFile.readLines()
|
||||||
val main = codeLines.indexOfFirst { it.startsWith("fun main") }
|
val main = codeLines.indexOfFirst { it.startsWith("fun main") }
|
||||||
val head = codeLines.take(main)
|
val head = codeLines.take(main)
|
||||||
val start = head.indexOfLast { it.startsWith("/**") }
|
val start = head.indexOfLast { it.startsWith("/*") }
|
||||||
val end = head.indexOfLast { it.endsWith("*/") }
|
val end = head.indexOfLast { it.endsWith("*/") }
|
||||||
|
|
||||||
if ((start < end) && (end < main)) {
|
if ((start < end) && (end < main)) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ kotlin {
|
|||||||
implementation(project(":orx-marching-squares"))
|
implementation(project(":orx-marching-squares"))
|
||||||
implementation(project(":orx-text-writer"))
|
implementation(project(":orx-text-writer"))
|
||||||
implementation(project(":orx-obj-loader"))
|
implementation(project(":orx-obj-loader"))
|
||||||
|
implementation(project(":orx-palette"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
desktop/src/jvmDemo/kotlin/DemoColorBrewer2.kt
Normal file
35
desktop/src/jvmDemo/kotlin/DemoColorBrewer2.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to use a ColorBrewer2 palette.
|
||||||
|
* Finds the first available palette with 5 colors,
|
||||||
|
* then draws concentric circles filled with those colors.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
val palette = colorBrewer2Palettes(
|
||||||
|
numberOfColors = 6,
|
||||||
|
paletteType = ColorBrewer2Type.Any
|
||||||
|
).first().colors
|
||||||
|
val cellSize = 50.0
|
||||||
|
extend {
|
||||||
|
palette.forEachIndexed { i, color ->
|
||||||
|
drawer.fill = color
|
||||||
|
drawer.rectangle(
|
||||||
|
Rectangle(
|
||||||
|
x = 0.0,
|
||||||
|
y = cellSize * i,
|
||||||
|
width = cellSize,
|
||||||
|
height = cellSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// drawer.circle(drawer.bounds.center, 300.0 - i * 40.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import org.openrndr.draw.loadFont
|
|||||||
import org.openrndr.extra.camera.Camera2D
|
import org.openrndr.extra.camera.Camera2D
|
||||||
import org.openrndr.extra.marchingsquares.findContours
|
import org.openrndr.extra.marchingsquares.findContours
|
||||||
import org.openrndr.extra.noise.gradientPerturbFractal
|
import org.openrndr.extra.noise.gradientPerturbFractal
|
||||||
import org.openrndr.extra.noise.simplex
|
|
||||||
import org.openrndr.extra.textwriter.writer
|
import org.openrndr.extra.textwriter.writer
|
||||||
import org.openrndr.extra.triangulation.DelaunayTriangulation
|
import org.openrndr.extra.triangulation.DelaunayTriangulation
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
@@ -18,7 +17,6 @@ import org.openrndr.math.Vector3
|
|||||||
import org.openrndr.shape.Segment2D
|
import org.openrndr.shape.Segment2D
|
||||||
import org.openrndr.shape.Segment3D
|
import org.openrndr.shape.Segment3D
|
||||||
import org.openrndr.shape.ShapeContour
|
import org.openrndr.shape.ShapeContour
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -30,7 +28,7 @@ import kotlin.random.Random
|
|||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
height = 720
|
height = 480
|
||||||
title = "Delaunator"
|
title = "Delaunator"
|
||||||
}
|
}
|
||||||
program {
|
program {
|
||||||
@@ -62,6 +60,10 @@ fun main() = application {
|
|||||||
extend(Camera2D())
|
extend(Camera2D())
|
||||||
println("draw")
|
println("draw")
|
||||||
var targetHeight: Double = zs.average()
|
var targetHeight: Double = zs.average()
|
||||||
|
val step = zs.max() - zs.min() / 6
|
||||||
|
var heightList = (0..5).map { index ->
|
||||||
|
zs.min() + step * index
|
||||||
|
}
|
||||||
var logEnabled = true
|
var logEnabled = true
|
||||||
var useInterpolation = false
|
var useInterpolation = false
|
||||||
var sampleLinear = false
|
var sampleLinear = false
|
||||||
@@ -171,12 +173,41 @@ fun main() = application {
|
|||||||
cellSize = 4.0,
|
cellSize = 4.0,
|
||||||
useInterpolation = useInterpolation
|
useInterpolation = useInterpolation
|
||||||
)
|
)
|
||||||
|
val associateWith: List<List<ShapeContour>> = heightList.map { height ->
|
||||||
|
findContours(
|
||||||
|
f = {
|
||||||
|
val triangle = triangles.firstOrNull { triangle ->
|
||||||
|
isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||||
|
}
|
||||||
|
triangle ?: return@findContours 0.0
|
||||||
|
val interpolate = interpolateHeight(
|
||||||
|
point = it,
|
||||||
|
triangle = listOf(
|
||||||
|
triangle.x1,
|
||||||
|
triangle.x2,
|
||||||
|
triangle.x3,
|
||||||
|
).map {
|
||||||
|
Vector3(it.x, it.y, associate[it]!!)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interpolate.z - height
|
||||||
|
},
|
||||||
|
area = drawer.bounds,
|
||||||
|
cellSize = 4.0,
|
||||||
|
useInterpolation = useInterpolation
|
||||||
|
)
|
||||||
|
}
|
||||||
if (logEnabled) println("useInterpolation = $useInterpolation")
|
if (logEnabled) println("useInterpolation = $useInterpolation")
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
contours.forEach {
|
if (true) contours.forEach {
|
||||||
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
|
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
|
||||||
drawer.contour(if (sampleLinear) it.sampleLinear() else it)
|
drawer.contour(if (sampleLinear) it.sampleLinear() else it)
|
||||||
|
}
|
||||||
|
if (false) associateWith.forEachIndexed { index, contours ->
|
||||||
|
contours.forEach {
|
||||||
|
drawer.fill = colorBrewer2[index].colors.first().opacify(0.1)
|
||||||
|
drawer.contour(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
|
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
|
||||||
@@ -210,6 +241,24 @@ fun isPointInTriangle(point: Vector2, triangle: List<Vector2>): Boolean {
|
|||||||
alpha <= 1 && beta <= 1 && gamma <= 1
|
alpha <= 1 && beta <= 1 && gamma <= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
|
||||||
|
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||||
|
|
||||||
|
val (v1, v2, v3) = triangle
|
||||||
|
|
||||||
|
// 计算重心坐标
|
||||||
|
val denominator = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||||
|
if (denominator == 0.0) return false // 退化三角形
|
||||||
|
|
||||||
|
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denominator
|
||||||
|
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denominator
|
||||||
|
val gamma = 1.0 - alpha - beta
|
||||||
|
|
||||||
|
// 点在三角形内当且仅当所有重心坐标都在[0,1]范围内
|
||||||
|
return alpha >= 0 && beta >= 0 && gamma >= 0 &&
|
||||||
|
alpha <= 1 && beta <= 1 && gamma <= 1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用重心坐标计算点在三角形上的高度
|
* 使用重心坐标计算点在三角形上的高度
|
||||||
* @param point 二维点 (x, y)
|
* @param point 二维点 (x, y)
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ import org.openrndr.draw.loadFont
|
|||||||
import org.openrndr.draw.shadeStyle
|
import org.openrndr.draw.shadeStyle
|
||||||
import org.openrndr.extra.camera.Orbital
|
import org.openrndr.extra.camera.Orbital
|
||||||
import org.openrndr.extra.marchingsquares.findContours
|
import org.openrndr.extra.marchingsquares.findContours
|
||||||
import org.openrndr.extra.noise.gradientPerturbFractal
|
|
||||||
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
|
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
|
||||||
import org.openrndr.extra.textwriter.writer
|
import org.openrndr.extra.textwriter.writer
|
||||||
import org.openrndr.extra.triangulation.DelaunayTriangulation
|
import org.openrndr.extra.triangulation.DelaunayTriangulation
|
||||||
|
import org.openrndr.extra.triangulation.DelaunayTriangulation3D
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.shape.Path3D
|
import org.openrndr.shape.Path3D
|
||||||
import org.openrndr.shape.Segment3D
|
import org.openrndr.shape.Segment3D
|
||||||
import org.openrndr.shape.ShapeContour
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author tabidachinokaze
|
* @author tabidachinokaze
|
||||||
@@ -48,15 +47,16 @@ fun main() = application {
|
|||||||
height = height,
|
height = height,
|
||||||
volcanoCount = 3
|
volcanoCount = 3
|
||||||
)*/
|
)*/
|
||||||
val points3D = coordinateGenerate(width, height).map {
|
val points3D = coordinateGenerate(width, height).map {
|
||||||
it.copy(x = it.x - width / 2, y = it.y - height / 2)
|
it.copy(x = it.x - width / 2, y = it.y - height / 2, z = it.z * 10)
|
||||||
}
|
}
|
||||||
val zs = points3D.map { it.z }
|
val zs = points3D.map { it.z }
|
||||||
println("zs = ${zs}")
|
println("zs = ${zs}")
|
||||||
val associate: MutableMap<Vector2, Double> = points3D.associate {
|
val associate: MutableMap<Vector2, Double> = points3D.associate {
|
||||||
Vector2(it.x, it.y) to it.z
|
Vector2(it.x, it.y) to it.z
|
||||||
}.toMutableMap()
|
}.toMutableMap()
|
||||||
val delaunay = DelaunayTriangulation(associate.map { it.key })
|
val delaunay = DelaunayTriangulation(associate.map { it.key })
|
||||||
|
val delaunay3D = DelaunayTriangulation3D(points3D.map { Vector3(it.x, it.y, it.z) })
|
||||||
|
|
||||||
//println(points3D.niceStr())
|
//println(points3D.niceStr())
|
||||||
//extend(Camera2D())
|
//extend(Camera2D())
|
||||||
@@ -84,7 +84,7 @@ fun main() = application {
|
|||||||
val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj")
|
val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj")
|
||||||
|
|
||||||
extend {
|
extend {
|
||||||
val triangles = delaunay.triangles()
|
val triangles = delaunay3D.triangles()
|
||||||
val segments = mutableListOf<Segment3D>()
|
val segments = mutableListOf<Segment3D>()
|
||||||
drawer.clear(ColorRGBa.BLACK)
|
drawer.clear(ColorRGBa.BLACK)
|
||||||
val indexDiff = (frameCount / 1000) % triangles.size
|
val indexDiff = (frameCount / 1000) % triangles.size
|
||||||
@@ -95,10 +95,12 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
|
drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
|
||||||
|
|
||||||
|
// 绘制等高线段区域
|
||||||
for ((i, triangle) in triangles.withIndex()) {
|
for ((i, triangle) in triangles.withIndex()) {
|
||||||
val segment2DS = triangle.contour.segments.filter {
|
val segment2DS = triangle.segments.filter {
|
||||||
val startZ = associate[it.start]!!
|
val startZ = it.start.z
|
||||||
val endZ = associate[it.end]!!
|
val endZ = it.end.z
|
||||||
if (startZ < endZ) {
|
if (startZ < endZ) {
|
||||||
targetHeight in startZ..endZ
|
targetHeight in startZ..endZ
|
||||||
} else {
|
} else {
|
||||||
@@ -108,8 +110,8 @@ fun main() = application {
|
|||||||
|
|
||||||
if (segment2DS.size == 2) {
|
if (segment2DS.size == 2) {
|
||||||
val vector2s = segment2DS.map {
|
val vector2s = segment2DS.map {
|
||||||
val startZ = associate[it.start]!!
|
val startZ = it.start.z
|
||||||
val endZ = associate[it.end]!!
|
val endZ = it.end.z
|
||||||
val start = Vector3(it.start.x, it.start.y, startZ)
|
val start = Vector3(it.start.x, it.start.y, startZ)
|
||||||
val end = Vector3(it.end.x, it.end.y, endZ)
|
val end = Vector3(it.end.x, it.end.y, endZ)
|
||||||
if (startZ < endZ) {
|
if (startZ < endZ) {
|
||||||
@@ -130,14 +132,8 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
drawer.strokeWeight = 20.0
|
drawer.strokeWeight = 20.0
|
||||||
drawer.stroke = ColorRGBa.PINK
|
drawer.stroke = ColorRGBa.PINK
|
||||||
val segment3DS = triangle.contour.segments.map {
|
|
||||||
val startZ = associate[it.start]!!
|
|
||||||
val endZ = associate[it.end]!!
|
|
||||||
Segment3D(it.start.vector3(z = startZ), it.end.vector3(z = endZ))
|
|
||||||
}
|
|
||||||
|
|
||||||
//drawer.contour(triangle.contour)
|
//drawer.contour(triangle.contour)
|
||||||
drawer.path(Path3D.fromSegments(segment3DS, closed = true))
|
drawer.path(triangle.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sorted = connectAllSegments(segments)
|
val sorted = connectAllSegments(segments)
|
||||||
@@ -161,6 +157,7 @@ fun main() = application {
|
|||||||
drawer.fill = ColorRGBa.YELLOW
|
drawer.fill = ColorRGBa.YELLOW
|
||||||
// if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
|
// if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
|
||||||
}
|
}
|
||||||
|
// 结束绘制等高线
|
||||||
/*for (y in 0 until (area.height / cellSize).toInt()) {
|
/*for (y in 0 until (area.height / cellSize).toInt()) {
|
||||||
for (x in 0 until (area.width / cellSize).toInt()) {
|
for (x in 0 until (area.width / cellSize).toInt()) {
|
||||||
values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
|
values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
|
||||||
@@ -169,7 +166,7 @@ fun main() = application {
|
|||||||
val contours = findContours(
|
val contours = findContours(
|
||||||
f = {
|
f = {
|
||||||
val triangle = triangles.firstOrNull { triangle ->
|
val triangle = triangles.firstOrNull { triangle ->
|
||||||
isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
|
isPointInTriangle3D(it, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||||
}
|
}
|
||||||
triangle ?: return@findContours 0.0
|
triangle ?: return@findContours 0.0
|
||||||
val interpolate = interpolateHeight(
|
val interpolate = interpolateHeight(
|
||||||
@@ -178,9 +175,7 @@ fun main() = application {
|
|||||||
triangle.x1,
|
triangle.x1,
|
||||||
triangle.x2,
|
triangle.x2,
|
||||||
triangle.x3,
|
triangle.x3,
|
||||||
).map {
|
)
|
||||||
Vector3(it.x, it.y, associate[it]!!)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
interpolate.z - targetHeight
|
interpolate.z - targetHeight
|
||||||
},
|
},
|
||||||
@@ -191,6 +186,7 @@ fun main() = application {
|
|||||||
if (logEnabled) println("useInterpolation = $useInterpolation")
|
if (logEnabled) println("useInterpolation = $useInterpolation")
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
contours.map {
|
contours.map {
|
||||||
|
if (false) drawer.contour(it)
|
||||||
it.segments.map {
|
it.segments.map {
|
||||||
Segment3D(
|
Segment3D(
|
||||||
it.start.vector3(),
|
it.start.vector3(),
|
||||||
|
|||||||
1
icegps-triangulation/.gitignore
vendored
Normal file
1
icegps-triangulation/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
16
icegps-triangulation/build.gradle.kts
Normal file
16
icegps-triangulation/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":math"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,596 @@
|
|||||||
|
package com.icegps.triangulation
|
||||||
|
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
val EPSILON: Double = 2.0.pow(-52)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Kotlin port of Mapbox's Delaunator incredibly fast JavaScript library for Delaunay triangulation of 2D points.
|
||||||
|
*
|
||||||
|
* @description Port of Mapbox's Delaunator (JavaScript) library - https://github.com/mapbox/delaunator
|
||||||
|
* @property coords flat positions' array - [x0, y0, x1, y1..]
|
||||||
|
*
|
||||||
|
* @since f0ed80d - commit
|
||||||
|
* @author Ricardo Matias
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class Delaunator(val coords: DoubleArray) {
|
||||||
|
val EDGE_STACK = IntArray(512)
|
||||||
|
|
||||||
|
private var count = coords.size shr 1
|
||||||
|
|
||||||
|
// arrays that will store the triangulation graph
|
||||||
|
val maxTriangles = (2 * count - 5).coerceAtLeast(0)
|
||||||
|
private val _triangles = IntArray(maxTriangles * 3)
|
||||||
|
private val _halfedges = IntArray(maxTriangles * 3)
|
||||||
|
|
||||||
|
lateinit var triangles: IntArray
|
||||||
|
lateinit var halfedges: IntArray
|
||||||
|
|
||||||
|
// temporary arrays for tracking the edges of the advancing convex hull
|
||||||
|
private var hashSize = ceil(sqrt(count * 1.0)).toInt()
|
||||||
|
private var hullPrev = IntArray(count) // edge to prev edge
|
||||||
|
private var hullNext = IntArray(count) // edge to next edge
|
||||||
|
private var hullTri = IntArray(count) // edge to adjacent triangle
|
||||||
|
private var hullHash = IntArray(hashSize) // angular edge hash
|
||||||
|
private var hullStart: Int = -1
|
||||||
|
|
||||||
|
// temporary arrays for sorting points
|
||||||
|
private var ids = IntArray(count)
|
||||||
|
private var dists = DoubleArray(count)
|
||||||
|
|
||||||
|
private var cx: Double = Double.NaN
|
||||||
|
private var cy: Double = Double.NaN
|
||||||
|
|
||||||
|
private var trianglesLen: Int = -1
|
||||||
|
|
||||||
|
lateinit var hull: IntArray
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
if (coords.size <= 2) {
|
||||||
|
halfedges = IntArray(0)
|
||||||
|
triangles = IntArray(0)
|
||||||
|
hull = IntArray(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// populate an array of point indices calculate input data bbox
|
||||||
|
var minX = Double.POSITIVE_INFINITY
|
||||||
|
var minY = Double.POSITIVE_INFINITY
|
||||||
|
var maxX = Double.NEGATIVE_INFINITY
|
||||||
|
var maxY = Double.NEGATIVE_INFINITY
|
||||||
|
|
||||||
|
// points -> points
|
||||||
|
// minX, minY, maxX, maxY
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val x = coords[2 * i]
|
||||||
|
val y = coords[2 * i + 1]
|
||||||
|
if (x < minX) minX = x
|
||||||
|
if (y < minY) minY = y
|
||||||
|
if (x > maxX) maxX = x
|
||||||
|
if (y > maxY) maxY = y
|
||||||
|
|
||||||
|
ids[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
val cx = (minX + maxX) / 2
|
||||||
|
val cy = (minY + maxY) / 2
|
||||||
|
|
||||||
|
var minDist = Double.POSITIVE_INFINITY
|
||||||
|
|
||||||
|
var i0: Int = -1
|
||||||
|
var i1: Int = -1
|
||||||
|
var i2: Int = -1
|
||||||
|
|
||||||
|
// pick a seed point close to the center
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val d = dist(cx, cy, coords[2 * i], coords[2 * i + 1])
|
||||||
|
|
||||||
|
if (d < minDist) {
|
||||||
|
i0 = i
|
||||||
|
minDist = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val i0x = coords[2 * i0]
|
||||||
|
val i0y = coords[2 * i0 + 1]
|
||||||
|
|
||||||
|
minDist = Double.POSITIVE_INFINITY
|
||||||
|
|
||||||
|
// Find the point closest to the seed
|
||||||
|
for(i in 0 until count) {
|
||||||
|
if (i == i0) continue
|
||||||
|
|
||||||
|
val d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1])
|
||||||
|
|
||||||
|
if (d < minDist && d > 0) {
|
||||||
|
i1 = i
|
||||||
|
minDist = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var i1x = coords[2 * i1]
|
||||||
|
var i1y = coords[2 * i1 + 1]
|
||||||
|
|
||||||
|
var minRadius = Double.POSITIVE_INFINITY
|
||||||
|
|
||||||
|
// Find the third point which forms the smallest circumcircle with the first two
|
||||||
|
for (i in 0 until count) {
|
||||||
|
if(i == i0 || i == i1) continue
|
||||||
|
|
||||||
|
val r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1])
|
||||||
|
|
||||||
|
if(r < minRadius) {
|
||||||
|
i2 = i
|
||||||
|
minRadius = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minRadius == Double.POSITIVE_INFINITY) {
|
||||||
|
// order collinear points by dx (or dy if all x are identical)
|
||||||
|
// and return the list as a hull
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val a = (coords[2 * i] - coords[0])
|
||||||
|
val b = (coords[2 * i + 1] - coords[1])
|
||||||
|
dists[i] = if (a == 0.0) b else a
|
||||||
|
}
|
||||||
|
|
||||||
|
quicksort(ids, dists, 0, count - 1)
|
||||||
|
|
||||||
|
val nhull = IntArray(count)
|
||||||
|
var j = 0
|
||||||
|
var d0 = Double.NEGATIVE_INFINITY
|
||||||
|
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val id = ids[i]
|
||||||
|
if (dists[id] > d0) {
|
||||||
|
nhull[j++] = id
|
||||||
|
d0 = dists[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hull = nhull.copyOf(j)
|
||||||
|
triangles = IntArray(0)
|
||||||
|
halfedges = IntArray(0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var i2x = coords[2 * i2]
|
||||||
|
var i2y = coords[2 * i2 + 1]
|
||||||
|
|
||||||
|
// swap the order of the seed points for counter-clockwise orientation
|
||||||
|
if (orient2d(i0x, i0y, i1x, i1y, i2x, i2y) < 0.0) {
|
||||||
|
val i = i1
|
||||||
|
val x = i1x
|
||||||
|
val y = i1y
|
||||||
|
i1 = i2
|
||||||
|
i1x = i2x
|
||||||
|
i1y = i2y
|
||||||
|
i2 = i
|
||||||
|
i2x = x
|
||||||
|
i2y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y)
|
||||||
|
|
||||||
|
this.cx = center[0]
|
||||||
|
this.cy = center[1]
|
||||||
|
|
||||||
|
for (i in 0 until count) {
|
||||||
|
dists[i] = dist(coords[2 * i], coords[2 * i + 1], center[0], center[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the points by distance from the seed triangle circumcenter
|
||||||
|
quicksort(ids, dists, 0, count - 1)
|
||||||
|
|
||||||
|
// set up the seed triangle as the starting hull
|
||||||
|
hullStart = i0
|
||||||
|
var hullSize = 3
|
||||||
|
|
||||||
|
hullNext[i0] = i1
|
||||||
|
hullNext[i1] = i2
|
||||||
|
hullNext[i2] = i0
|
||||||
|
|
||||||
|
hullPrev[i2] = i1
|
||||||
|
hullPrev[i0] = i2
|
||||||
|
hullPrev[i1] = i0
|
||||||
|
|
||||||
|
hullTri[i0] = 0
|
||||||
|
hullTri[i1] = 1
|
||||||
|
hullTri[i2] = 2
|
||||||
|
|
||||||
|
hullHash.fill(-1)
|
||||||
|
hullHash[hashKey(i0x, i0y)] = i0
|
||||||
|
hullHash[hashKey(i1x, i1y)] = i1
|
||||||
|
hullHash[hashKey(i2x, i2y)] = i2
|
||||||
|
|
||||||
|
trianglesLen = 0
|
||||||
|
addTriangle(i0, i1, i2, -1, -1, -1)
|
||||||
|
|
||||||
|
var xp = 0.0
|
||||||
|
var yp = 0.0
|
||||||
|
|
||||||
|
for (k in ids.indices) {
|
||||||
|
val i = ids[k]
|
||||||
|
val x = coords[2 * i]
|
||||||
|
val y = coords[2 * i + 1]
|
||||||
|
|
||||||
|
// skip near-duplicate points
|
||||||
|
if (k > 0 && abs(x - xp) <= EPSILON && abs(y - yp) <= EPSILON) continue
|
||||||
|
|
||||||
|
xp = x
|
||||||
|
yp = y
|
||||||
|
|
||||||
|
// skip seed triangle points
|
||||||
|
if (i == i0 || i == i1 || i == i2) continue
|
||||||
|
|
||||||
|
// find a visible edge on the convex hull using edge hash
|
||||||
|
var start = 0
|
||||||
|
val key = hashKey(x, y)
|
||||||
|
|
||||||
|
for (j in 0 until hashSize) {
|
||||||
|
start = hullHash[(key + j) % hashSize]
|
||||||
|
|
||||||
|
if (start != -1 && start != hullNext[start]) break
|
||||||
|
}
|
||||||
|
|
||||||
|
start = hullPrev[start]
|
||||||
|
|
||||||
|
var e = start
|
||||||
|
var q = hullNext[e]
|
||||||
|
|
||||||
|
while (orient2d(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1]) >= 0) {
|
||||||
|
e = q
|
||||||
|
|
||||||
|
if (e == start) {
|
||||||
|
e = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
q = hullNext[e]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e == -1) continue // likely a near-duplicate point skip it
|
||||||
|
|
||||||
|
// add the first triangle from the point
|
||||||
|
var t = addTriangle(e, i, hullNext[e], -1, -1, hullTri[e])
|
||||||
|
|
||||||
|
// recursively flip triangles from the point until they satisfy the Delaunay condition
|
||||||
|
hullTri[i] = legalize(t + 2)
|
||||||
|
hullTri[e] = t // keep track of boundary triangles on the hull
|
||||||
|
hullSize++
|
||||||
|
|
||||||
|
// walk forward through the hull, adding more triangles and flipping recursively
|
||||||
|
var next = hullNext[e]
|
||||||
|
q = hullNext[next]
|
||||||
|
|
||||||
|
while (orient2d(x, y, coords[2 * next], coords[2 * next + 1], coords[2 * q], coords[2 * q + 1]) < 0) {
|
||||||
|
t = addTriangle(next, i, q, hullTri[i], -1, hullTri[next])
|
||||||
|
hullTri[i] = legalize(t + 2)
|
||||||
|
hullNext[next] = next // mark as removed
|
||||||
|
hullSize--
|
||||||
|
|
||||||
|
next = q
|
||||||
|
q = hullNext[next]
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk backward from the other side, adding more triangles and flipping
|
||||||
|
if (e == start) {
|
||||||
|
q = hullPrev[e]
|
||||||
|
|
||||||
|
while (orient2d(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1]) < 0) {
|
||||||
|
t = addTriangle(q, i, e, -1, hullTri[e], hullTri[q])
|
||||||
|
legalize(t + 2)
|
||||||
|
hullTri[q] = t
|
||||||
|
hullNext[e] = e // mark as removed
|
||||||
|
hullSize--
|
||||||
|
|
||||||
|
e = q
|
||||||
|
q = hullPrev[e]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the hull indices
|
||||||
|
hullStart = e
|
||||||
|
hullPrev[i] = e
|
||||||
|
|
||||||
|
hullNext[e] = i
|
||||||
|
hullPrev[next] = i
|
||||||
|
hullNext[i] = next
|
||||||
|
|
||||||
|
// save the two new edges in the hash table
|
||||||
|
hullHash[hashKey(x, y)] = i
|
||||||
|
hullHash[hashKey(coords[2 * e], coords[2 * e + 1])] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
hull = IntArray(hullSize)
|
||||||
|
|
||||||
|
var e = hullStart
|
||||||
|
|
||||||
|
for (i in 0 until hullSize) {
|
||||||
|
hull[i] = e
|
||||||
|
e = hullNext[e]
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim typed triangle mesh arrays
|
||||||
|
triangles = _triangles.copyOf(trianglesLen)
|
||||||
|
halfedges = _halfedges.copyOf(trianglesLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun legalize(a: Int): Int {
|
||||||
|
var i = 0
|
||||||
|
var na = a
|
||||||
|
var ar: Int
|
||||||
|
|
||||||
|
// recursion eliminated with a fixed-size stack
|
||||||
|
while (true) {
|
||||||
|
val b = _halfedges[na]
|
||||||
|
|
||||||
|
/* if the pair of triangles doesn't satisfy the Delaunay condition
|
||||||
|
* (p1 is inside the circumcircle of [p0, pl, pr]), flip them,
|
||||||
|
* then do the same check/flip recursively for the new pair of triangles
|
||||||
|
*
|
||||||
|
* pl pl
|
||||||
|
* /||\ / \
|
||||||
|
* al/ || \bl al/ \a
|
||||||
|
* / || \ / \
|
||||||
|
* / a||b \ flip /___ar___\
|
||||||
|
* p0\ || /p1 => p0\---bl---/p1
|
||||||
|
* \ || / \ /
|
||||||
|
* ar\ || /br b\ /br
|
||||||
|
* \||/ \ /
|
||||||
|
* pr pr
|
||||||
|
*/
|
||||||
|
val a0 = na - na % 3
|
||||||
|
ar = a0 + (na + 2) % 3
|
||||||
|
|
||||||
|
if (b == -1) { // convex hull edge
|
||||||
|
if (i == 0) break
|
||||||
|
na = EDGE_STACK[--i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val b0 = b - b % 3
|
||||||
|
val al = a0 + (na + 1) % 3
|
||||||
|
val bl = b0 + (b + 2) % 3
|
||||||
|
|
||||||
|
val p0 = _triangles[ar]
|
||||||
|
val pr = _triangles[na]
|
||||||
|
val pl = _triangles[al]
|
||||||
|
val p1 = _triangles[bl]
|
||||||
|
|
||||||
|
val illegal = inCircleRobust(
|
||||||
|
coords[2 * p0], coords[2 * p0 + 1],
|
||||||
|
coords[2 * pr], coords[2 * pr + 1],
|
||||||
|
coords[2 * pl], coords[2 * pl + 1],
|
||||||
|
coords[2 * p1], coords[2 * p1 + 1])
|
||||||
|
|
||||||
|
if (illegal) {
|
||||||
|
_triangles[na] = p1
|
||||||
|
_triangles[b] = p0
|
||||||
|
|
||||||
|
val hbl = _halfedges[bl]
|
||||||
|
|
||||||
|
// edge swapped on the other side of the hull (rare) fix the halfedge reference
|
||||||
|
if (hbl == -1) {
|
||||||
|
var e = hullStart
|
||||||
|
do {
|
||||||
|
if (hullTri[e] == bl) {
|
||||||
|
hullTri[e] = na
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e = hullPrev[e]
|
||||||
|
} while (e != hullStart)
|
||||||
|
}
|
||||||
|
link(na, hbl)
|
||||||
|
link(b, _halfedges[ar])
|
||||||
|
link(ar, bl)
|
||||||
|
|
||||||
|
val br = b0 + (b + 1) % 3
|
||||||
|
|
||||||
|
// don't worry about hitting the cap: it can only happen on extremely degenerate input
|
||||||
|
if (i < EDGE_STACK.size) {
|
||||||
|
EDGE_STACK[i++] = br
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i == 0) break
|
||||||
|
na = EDGE_STACK[--i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ar
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun link(a:Int, b:Int) {
|
||||||
|
_halfedges[a] = b
|
||||||
|
if (b != -1) _halfedges[b] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a new triangle given vertex indices and adjacent half-edge ids
|
||||||
|
private fun addTriangle(i0: Int, i1: Int, i2: Int, a: Int, b: Int, c: Int): Int {
|
||||||
|
val t = trianglesLen
|
||||||
|
|
||||||
|
_triangles[t] = i0
|
||||||
|
_triangles[t + 1] = i1
|
||||||
|
_triangles[t + 2] = i2
|
||||||
|
|
||||||
|
link(t, a)
|
||||||
|
link(t + 1, b)
|
||||||
|
link(t + 2, c)
|
||||||
|
|
||||||
|
trianglesLen += 3
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hashKey(x: Double, y: Double): Int {
|
||||||
|
return (floor(pseudoAngle(x - cx, y - cy) * hashSize) % hashSize).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun circumradius(ax: Double, ay: Double,
|
||||||
|
bx: Double, by: Double,
|
||||||
|
cx: Double, cy: Double): Double {
|
||||||
|
val dx = bx - ax
|
||||||
|
val dy = by - ay
|
||||||
|
val ex = cx - ax
|
||||||
|
val ey = cy - ay
|
||||||
|
|
||||||
|
val bl = dx * dx + dy * dy
|
||||||
|
val cl = ex * ex + ey * ey
|
||||||
|
val d = 0.5 / (dx * ey - dy * ex)
|
||||||
|
|
||||||
|
val x = (ey * bl - dy * cl) * d
|
||||||
|
val y = (dx * cl - ex * bl) * d
|
||||||
|
|
||||||
|
return x * x + y * y
|
||||||
|
}
|
||||||
|
|
||||||
|
fun circumcenter(ax: Double, ay: Double,
|
||||||
|
bx: Double, by: Double,
|
||||||
|
cx: Double, cy: Double): DoubleArray {
|
||||||
|
val dx = bx - ax
|
||||||
|
val dy = by - ay
|
||||||
|
val ex = cx - ax
|
||||||
|
val ey = cy - ay
|
||||||
|
|
||||||
|
val bl = dx * dx + dy * dy
|
||||||
|
val cl = ex * ex + ey * ey
|
||||||
|
val d = 0.5 / (dx * ey - dy * ex)
|
||||||
|
|
||||||
|
val x = ax + (ey * bl - dy * cl) * d
|
||||||
|
val y = ay + (dx * cl - ex * bl) * d
|
||||||
|
|
||||||
|
return doubleArrayOf(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun quicksort(ids: IntArray, dists: DoubleArray, left: Int, right: Int) {
|
||||||
|
if (right - left <= 20) {
|
||||||
|
for (i in (left + 1)..right) {
|
||||||
|
val temp = ids[i]
|
||||||
|
val tempDist = dists[temp]
|
||||||
|
var j = i - 1
|
||||||
|
while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]
|
||||||
|
ids[j + 1] = temp
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val median = (left + right) shr 1
|
||||||
|
var i = left + 1
|
||||||
|
var j = right
|
||||||
|
|
||||||
|
swap(ids, median, i)
|
||||||
|
|
||||||
|
if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right)
|
||||||
|
if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right)
|
||||||
|
if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i)
|
||||||
|
|
||||||
|
val temp = ids[i]
|
||||||
|
val tempDist = dists[temp]
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
do i++ while (dists[ids[i]] < tempDist)
|
||||||
|
do j-- while (dists[ids[j]] > tempDist)
|
||||||
|
if (j < i) break
|
||||||
|
swap(ids, i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids[left + 1] = ids[j]
|
||||||
|
ids[j] = temp
|
||||||
|
|
||||||
|
if (right - i + 1 >= j - left) {
|
||||||
|
quicksort(ids, dists, i, right)
|
||||||
|
quicksort(ids, dists, left, j - 1)
|
||||||
|
} else {
|
||||||
|
quicksort(ids, dists, left, j - 1)
|
||||||
|
quicksort(ids, dists, i, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun swap(arr: IntArray, i: Int, j: Int) {
|
||||||
|
val tmp = arr[i]
|
||||||
|
arr[i] = arr[j]
|
||||||
|
arr[j] = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// monotonically increases with real angle, but doesn't need expensive trigonometry
|
||||||
|
private fun pseudoAngle(dx: Double, dy: Double): Double {
|
||||||
|
val p = dx / (abs(dx) + abs(dy))
|
||||||
|
val a = if (dy > 0.0) 3.0 - p else 1.0 + p
|
||||||
|
|
||||||
|
return a / 4.0 // [0..1]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inCircle(ax: Double, ay: Double,
|
||||||
|
bx: Double, by: Double,
|
||||||
|
cx: Double, cy: Double,
|
||||||
|
px: Double, py: Double): Boolean {
|
||||||
|
val dx = ax - px
|
||||||
|
val dy = ay - py
|
||||||
|
val ex = bx - px
|
||||||
|
val ey = by - py
|
||||||
|
val fx = cx - px
|
||||||
|
val fy = cy - py
|
||||||
|
|
||||||
|
val ap = dx * dx + dy * dy
|
||||||
|
val bp = ex * ex + ey * ey
|
||||||
|
val cp = fx * fx + fy * fy
|
||||||
|
|
||||||
|
return dx * (ey * cp - bp * fy) -
|
||||||
|
dy * (ex * cp - bp * fx) +
|
||||||
|
ap * (ex * fy - ey * fx) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inCircleRobust(
|
||||||
|
ax: Double, ay: Double,
|
||||||
|
bx: Double, by: Double,
|
||||||
|
cx: Double, cy: Double,
|
||||||
|
px: Double, py: Double
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val dx = twoDiff(ax, px)
|
||||||
|
val dy = twoDiff(ay, py)
|
||||||
|
val ex = twoDiff(bx, px)
|
||||||
|
val ey = twoDiff(by, py)
|
||||||
|
val fx = twoDiff(cx, px)
|
||||||
|
val fy = twoDiff(cy, py)
|
||||||
|
|
||||||
|
val ap = ddAddDd(ddMultDd(dx, dx), ddMultDd(dy, dy))
|
||||||
|
val bp = ddAddDd(ddMultDd(ex, ex), ddMultDd(ey, ey))
|
||||||
|
val cp = ddAddDd(ddMultDd(fx, fx), ddMultDd(fy, fy))
|
||||||
|
|
||||||
|
val dd = ddAddDd(
|
||||||
|
ddDiffDd(
|
||||||
|
ddMultDd(dx, ddDiffDd(ddMultDd(ey, cp), ddMultDd(bp, fy))),
|
||||||
|
ddMultDd(dy, ddDiffDd(ddMultDd(ex, cp), ddMultDd(bp, fx)))
|
||||||
|
),
|
||||||
|
ddMultDd(ap, ddDiffDd(ddMultDd(ex, fy), ddMultDd(ey, fx)))
|
||||||
|
)
|
||||||
|
return (dd[1]) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun dist(ax: Double, ay: Double, bx: Double, by: Double): Double {
|
||||||
|
//val dx = ax - bx
|
||||||
|
//val dy = ay - by
|
||||||
|
//return dx * dx + dy * dy
|
||||||
|
|
||||||
|
// double-double implementation but I think it is overkill.
|
||||||
|
|
||||||
|
val dx = twoDiff(ax, bx)
|
||||||
|
val dy = twoDiff(ay, by)
|
||||||
|
val dx2 = ddMultDd(dx, dx)
|
||||||
|
val dy2 = ddMultDd(dy, dy)
|
||||||
|
val d2 = ddAddDd(dx2, dy2)
|
||||||
|
|
||||||
|
return d2[0] + d2[1]
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package com.icegps.triangulation
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Vector2D
|
||||||
|
import com.icegps.triangulation.Delaunay.Companion.from
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [from] static method to use 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
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class Delaunay(val points: DoubleArray) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Entry point for the delaunay triangulation
|
||||||
|
*
|
||||||
|
* @property points a list of 2D points
|
||||||
|
*/
|
||||||
|
fun from(points: List<Vector2D>): Delaunay {
|
||||||
|
val n = points.size
|
||||||
|
val coords = DoubleArray(n * 2)
|
||||||
|
|
||||||
|
for (i in points.indices) {
|
||||||
|
val p = points[i]
|
||||||
|
coords[2 * i] = p.x
|
||||||
|
coords[2 * i + 1] = p.y
|
||||||
|
}
|
||||||
|
|
||||||
|
return Delaunay(coords)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var delaunator: Delaunator = Delaunator(points)
|
||||||
|
|
||||||
|
val inedges = IntArray(points.size / 2)
|
||||||
|
private val hullIndex = IntArray(points.size / 2)
|
||||||
|
|
||||||
|
var halfedges: IntArray = delaunator.halfedges
|
||||||
|
var hull: IntArray = delaunator.hull
|
||||||
|
var triangles: IntArray = delaunator.triangles
|
||||||
|
|
||||||
|
init {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
delaunator.update()
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun neighbors(i: Int) = sequence<Int> {
|
||||||
|
val e0 = inedges[i]
|
||||||
|
if (e0 != -1) {
|
||||||
|
var e = e0
|
||||||
|
var p0 = -1
|
||||||
|
|
||||||
|
loop@ do {
|
||||||
|
p0 = triangles[e]
|
||||||
|
yield(p0)
|
||||||
|
e = if (e % 3 == 2) e - 2 else e + 1
|
||||||
|
if (e == -1) {
|
||||||
|
break@loop
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triangles[e] != i) {
|
||||||
|
break@loop
|
||||||
|
//error("bad triangulation")
|
||||||
|
}
|
||||||
|
e = halfedges[e]
|
||||||
|
|
||||||
|
if (e == -1) {
|
||||||
|
val p = hull[(hullIndex[i] + 1) % hull.size]
|
||||||
|
if (p != p0) {
|
||||||
|
yield(p)
|
||||||
|
}
|
||||||
|
break@loop
|
||||||
|
}
|
||||||
|
} while (e != e0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun collinear(): Boolean {
|
||||||
|
for (i in 0 until triangles.size step 3) {
|
||||||
|
val a = 2 * triangles[i]
|
||||||
|
val b = 2 * triangles[i + 1]
|
||||||
|
val c = 2 * triangles[i + 2]
|
||||||
|
val coords = points
|
||||||
|
val cross = (coords[c] - coords[a]) * (coords[b + 1] - coords[a + 1])
|
||||||
|
-(coords[b] - coords[a]) * (coords[c + 1] - coords[a + 1])
|
||||||
|
if (cross > 1e-10) return false;
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun jitter(x: Double, y: Double, r: Double): DoubleArray {
|
||||||
|
return doubleArrayOf(x + sin(x + y) * r, y + cos(x - y) * r)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
|
||||||
|
if (hull.size > 2 && collinear()) {
|
||||||
|
println("warning: triangulation is collinear")
|
||||||
|
val r = 1E-8
|
||||||
|
for (i in 0 until points.size step 2) {
|
||||||
|
val p = jitter(points[i], points[i + 1], r)
|
||||||
|
points[i] = p[0]
|
||||||
|
points[i + 1] = p[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
delaunator = Delaunator(points)
|
||||||
|
halfedges = delaunator.halfedges
|
||||||
|
hull = delaunator.hull
|
||||||
|
triangles = delaunator.triangles
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
inedges.fill(-1)
|
||||||
|
hullIndex.fill(-1)
|
||||||
|
|
||||||
|
// Compute an index from each point to an (arbitrary) incoming halfedge
|
||||||
|
// Used to give the first neighbor of each point for this reason,
|
||||||
|
// on the hull we give priority to exterior halfedges
|
||||||
|
for (e in halfedges.indices) {
|
||||||
|
val p = triangles[nextHalfedge(e)]
|
||||||
|
|
||||||
|
if (halfedges[e] == -1 || inedges[p] == -1) inedges[p] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in hull.indices) {
|
||||||
|
hullIndex[hull[i]] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// degenerate case: 1 or 2 (distinct) points
|
||||||
|
if (hull.size in 1..2) {
|
||||||
|
triangles = IntArray(3) { -1 }
|
||||||
|
halfedges = IntArray(3) { -1 }
|
||||||
|
triangles[0] = hull[0]
|
||||||
|
inedges[hull[0]] = 1
|
||||||
|
if (hull.size == 2) {
|
||||||
|
inedges[hull[1]] = 0
|
||||||
|
triangles[1] = hull[1]
|
||||||
|
triangles[2] = hull[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun find(x: Double, y: Double, i: Int = 0): Int {
|
||||||
|
var i1 = i
|
||||||
|
var c = step(i, x, y)
|
||||||
|
|
||||||
|
while (c >= 0 && c != i && c != i1) {
|
||||||
|
i1 = c
|
||||||
|
c = step(i1, x, y)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextHalfedge(e: Int) = if (e % 3 == 2) e - 2 else e + 1
|
||||||
|
fun prevHalfedge(e: Int) = if (e % 3 == 0) e + 2 else e - 1
|
||||||
|
|
||||||
|
fun step(i: Int, x: Double, y: Double): Int {
|
||||||
|
if (inedges[i] == -1 || points.isEmpty()) return (i + 1) % (points.size shr 1)
|
||||||
|
|
||||||
|
var c = i
|
||||||
|
var dc = (x - points[i * 2]).pow(2) + (y - points[i * 2 + 1]).pow(2)
|
||||||
|
val e0 = inedges[i]
|
||||||
|
var e = e0
|
||||||
|
do {
|
||||||
|
val t = triangles[e]
|
||||||
|
val dt = (x - points[t * 2]).pow(2) + (y - points[t * 2 + 1]).pow(2)
|
||||||
|
|
||||||
|
if (dt < dc) {
|
||||||
|
dc = dt
|
||||||
|
c = t
|
||||||
|
}
|
||||||
|
|
||||||
|
e = if (e % 3 == 2) e - 2 else e + 1
|
||||||
|
|
||||||
|
if (triangles[e] != i) {
|
||||||
|
//error("bad triangulation")
|
||||||
|
break
|
||||||
|
} // bad triangulation
|
||||||
|
|
||||||
|
e = halfedges[e]
|
||||||
|
|
||||||
|
if (e == -1) {
|
||||||
|
e = hull[(hullIndex[i] + 1) % hull.size]
|
||||||
|
if (e != t) {
|
||||||
|
if ((x - points[e * 2]).pow(2) + (y - points[e * 2 + 1]).pow(2) < dc) return e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} while (e != e0)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.icegps.triangulation
|
||||||
|
|
||||||
|
import com.icegps.math.geometry.Vector3D
|
||||||
|
import com.icegps.math.geometry.toVector2D
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin/OPENRNDR idiomatic interface to `Delaunay`
|
||||||
|
*/
|
||||||
|
class DelaunayTriangulation(val points: List<Vector3D>) {
|
||||||
|
val delaunay: Delaunay = Delaunay.from(points.map { it.toVector2D() })
|
||||||
|
|
||||||
|
fun neighbors(pointIndex: Int): Sequence<Int> {
|
||||||
|
return delaunay.neighbors(pointIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun neighborPoints(pointIndex: Int): List<Vector3D> {
|
||||||
|
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 }): List<Triangle> {
|
||||||
|
val list = mutableListOf<Triangle>()
|
||||||
|
|
||||||
|
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(Triangle(p3, p2, p1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nearest(query: Vector3D): Int = delaunay.find(query.x, query.y)
|
||||||
|
|
||||||
|
fun nearestPoint(query: Vector3D): Vector3D = 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<Vector3D>.delaunayTriangulation(): DelaunayTriangulation {
|
||||||
|
return DelaunayTriangulation(this)
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
package com.icegps.triangulation
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
// original code: https://github.com/FlorisSteenkamp/double-double/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the difference and exact error of subtracting two floating point
|
||||||
|
* numbers.
|
||||||
|
* Uses an EFT (error-free transformation), i.e. `a-b === x+y` exactly.
|
||||||
|
* The returned result is a non-overlapping expansion (smallest value first!).
|
||||||
|
*
|
||||||
|
* * **precondition:** `abs(a) >= abs(b)` - A fast test that can be used is
|
||||||
|
* `(a > b) === (a > -b)`
|
||||||
|
*
|
||||||
|
* See https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf
|
||||||
|
*/
|
||||||
|
fun fastTwoDiff(a: Double, b: Double): DoubleArray {
|
||||||
|
val x = a - b;
|
||||||
|
val y = (a - x) - b;
|
||||||
|
|
||||||
|
return doubleArrayOf(y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sum and exact error of adding two floating point numbers.
|
||||||
|
* Uses an EFT (error-free transformation), i.e. a+b === x+y exactly.
|
||||||
|
* The returned sum is a non-overlapping expansion (smallest value first!).
|
||||||
|
*
|
||||||
|
* Precondition: abs(a) >= abs(b) - A fast test that can be used is
|
||||||
|
* (a > b) === (a > -b)
|
||||||
|
*
|
||||||
|
* See https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf
|
||||||
|
*/
|
||||||
|
fun fastTwoSum(a: Double, b: Double): DoubleArray {
|
||||||
|
val x = a + b;
|
||||||
|
|
||||||
|
return doubleArrayOf(b - (x - a), x)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a floating point value's significand and returns the result.
|
||||||
|
* Similar to split, but with the ability to specify the number of bits to keep.
|
||||||
|
*
|
||||||
|
* **Theorem 17 (Veltkamp-Dekker)**: Let a be a p-bit floating-point number, where
|
||||||
|
* p >= 3. Choose a splitting point s such that p/2 <= s <= p-1. Then the
|
||||||
|
* following algorithm will produce a (p-s)-bit value a_hi and a
|
||||||
|
* nonoverlapping (s-1)-bit value a_lo such that abs(a_hi) >= abs(a_lo) and
|
||||||
|
* a = a_hi + a_lo.
|
||||||
|
*
|
||||||
|
* * see [Shewchuk](https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf)
|
||||||
|
*
|
||||||
|
* @param a a double
|
||||||
|
* @param bits the number of significand bits to leave intact
|
||||||
|
*/
|
||||||
|
fun reduceSignificand(
|
||||||
|
a: Double,
|
||||||
|
bits: Int
|
||||||
|
): Double {
|
||||||
|
|
||||||
|
val s = 53 - bits;
|
||||||
|
val f = 2.0.pow(s) + 1;
|
||||||
|
|
||||||
|
val c = f * a;
|
||||||
|
val r = c - (c - a);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* === 2^Math.ceil(p/2) + 1 where p is the # of significand bits in a double === 53.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
const val f = 134217729; // 2**27 + 1;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of splitting a double into 2 26-bit doubles.
|
||||||
|
*
|
||||||
|
* Theorem 17 (Veltkamp-Dekker): Let a be a p-bit floating-point number, where
|
||||||
|
* p >= 3. Choose a splitting point s such that p/2 <= s <= p-1. Then the
|
||||||
|
* following algorithm will produce a (p-s)-bit value a_hi and a
|
||||||
|
* nonoverlapping (s-1)-bit value a_lo such that abs(a_hi) >= abs(a_lo) and
|
||||||
|
* a = a_hi + a_lo.
|
||||||
|
*
|
||||||
|
* see e.g. [Shewchuk](https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf)
|
||||||
|
* @param a A double floating point number
|
||||||
|
*/
|
||||||
|
fun split(a: Double): DoubleArray {
|
||||||
|
val c = f * a;
|
||||||
|
val a_h = c - (c - a);
|
||||||
|
val a_l = a - a_h;
|
||||||
|
|
||||||
|
return doubleArrayOf(a_h, a_l)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact result of subtracting b from a.
|
||||||
|
*
|
||||||
|
* @param a minuend - a double-double precision floating point number
|
||||||
|
* @param b subtrahend - a double-double precision floating point number
|
||||||
|
*/
|
||||||
|
fun twoDiff(a: Double, b: Double): DoubleArray {
|
||||||
|
val x = a - b;
|
||||||
|
val bvirt = a - x;
|
||||||
|
val y = (a - (x + bvirt)) + (bvirt - b);
|
||||||
|
|
||||||
|
return doubleArrayOf(y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact result of multiplying two doubles.
|
||||||
|
*
|
||||||
|
* * the resulting array is the reverse of the standard twoSum in the literature.
|
||||||
|
*
|
||||||
|
* Theorem 18 (Shewchuk): Let a and b be p-bit floating-point numbers, where
|
||||||
|
* p >= 6. Then the following algorithm will produce a nonoverlapping expansion
|
||||||
|
* x + y such that ab = x + y, where x is an approximation to ab and y
|
||||||
|
* represents the roundoff error in the calculation of x. Furthermore, if
|
||||||
|
* round-to-even tiebreaking is used, x and y are non-adjacent.
|
||||||
|
*
|
||||||
|
* See https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf
|
||||||
|
* @param a A double
|
||||||
|
* @param b Another double
|
||||||
|
*/
|
||||||
|
fun twoProduct(a: Double, b: Double): DoubleArray {
|
||||||
|
val x = a * b;
|
||||||
|
|
||||||
|
//const [ah, al] = split(a);
|
||||||
|
val c = f * a;
|
||||||
|
val ah = c - (c - a);
|
||||||
|
val al = a - ah;
|
||||||
|
//const [bh, bl] = split(b);
|
||||||
|
val d = f * b;
|
||||||
|
val bh = d - (d - b);
|
||||||
|
val bl = b - bh;
|
||||||
|
|
||||||
|
val y = (al * bl) - ((x - (ah * bh)) - (al * bh) - (ah * bl));
|
||||||
|
|
||||||
|
//const err1 = x - (ah * bh);
|
||||||
|
//const err2 = err1 - (al * bh);
|
||||||
|
//const err3 = err2 - (ah * bl);
|
||||||
|
//const y = (al * bl) - err3;
|
||||||
|
|
||||||
|
return doubleArrayOf(y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun twoSquare(a: Double): DoubleArray {
|
||||||
|
val x = a * a;
|
||||||
|
|
||||||
|
//const [ah, al] = split(a);
|
||||||
|
val c = f * a;
|
||||||
|
val ah = c - (c - a);
|
||||||
|
val al = a - ah;
|
||||||
|
|
||||||
|
val y = (al * al) - ((x - (ah * ah)) - 2 * (ah * al));
|
||||||
|
|
||||||
|
return doubleArrayOf(y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the exact result of adding two doubles.
|
||||||
|
*
|
||||||
|
* * the resulting array is the reverse of the standard twoSum in the literature.
|
||||||
|
*
|
||||||
|
* Theorem 7 (Knuth): Let a and b be p-bit floating-point numbers. Then the
|
||||||
|
* following algorithm will produce a nonoverlapping expansion x + y such that
|
||||||
|
* a + b = x + y, where x is an approximation to a + b and y is the roundoff
|
||||||
|
* error in the calculation of x.
|
||||||
|
*
|
||||||
|
* See https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf
|
||||||
|
*/
|
||||||
|
fun twoSum(a: Double, b: Double): DoubleArray {
|
||||||
|
val x = a + b;
|
||||||
|
val bv = x - a;
|
||||||
|
|
||||||
|
return doubleArrayOf((a - (x - bv)) + (b - bv), x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of subtracting the second given double-double-precision
|
||||||
|
* floating point number from the first.
|
||||||
|
*
|
||||||
|
* * relative error bound: 3u^2 + 13u^3, i.e. fl(a-b) = (a-b)(1+ϵ),
|
||||||
|
* where ϵ <= 3u^2 + 13u^3, u = 0.5 * Number.EPSILON
|
||||||
|
* * the error bound is not sharp - the worst case that could be found by the
|
||||||
|
* authors were 2.25u^2
|
||||||
|
*
|
||||||
|
* ALGORITHM 6 of https://hal.archives-ouvertes.fr/hal-01351529v3/document
|
||||||
|
* @param x a double-double precision floating point number
|
||||||
|
* @param y another double-double precision floating point number
|
||||||
|
*/
|
||||||
|
fun ddDiffDd(x: DoubleArray, y: DoubleArray): DoubleArray {
|
||||||
|
val xl = x[0];
|
||||||
|
val xh = x[1];
|
||||||
|
val yl = y[0];
|
||||||
|
val yh = y[1];
|
||||||
|
|
||||||
|
//const [sl,sh] = twoSum(xh,yh);
|
||||||
|
val sh = xh - yh;
|
||||||
|
val _1 = sh - xh;
|
||||||
|
val sl = (xh - (sh - _1)) + (-yh - _1);
|
||||||
|
//const [tl,th] = twoSum(xl,yl);
|
||||||
|
val th = xl - yl;
|
||||||
|
val _2 = th - xl;
|
||||||
|
val tl = (xl - (th - _2)) + (-yl - _2);
|
||||||
|
val c = sl + th;
|
||||||
|
//const [vl,vh] = fastTwoSum(sh,c)
|
||||||
|
val vh = sh + c;
|
||||||
|
val vl = c - (vh - sh);
|
||||||
|
val w = tl + vl
|
||||||
|
//const [zl,zh] = fastTwoSum(vh,w)
|
||||||
|
val zh = vh + w;
|
||||||
|
val zl = w - (zh - vh);
|
||||||
|
|
||||||
|
return doubleArrayOf(zl, zh)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the product of two double-double-precision floating point numbers.
|
||||||
|
*
|
||||||
|
* * relative error bound: 7u^2, i.e. fl(a+b) = (a+b)(1+ϵ),
|
||||||
|
* where ϵ <= 7u^2, u = 0.5 * Number.EPSILON
|
||||||
|
* the error bound is not sharp - the worst case that could be found by the
|
||||||
|
* authors were 5u^2
|
||||||
|
*
|
||||||
|
* * ALGORITHM 10 of https://hal.archives-ouvertes.fr/hal-01351529v3/document
|
||||||
|
* @param x a double-double precision floating point number
|
||||||
|
* @param y another double-double precision floating point number
|
||||||
|
*/
|
||||||
|
fun ddMultDd(x: DoubleArray, y: DoubleArray): DoubleArray {
|
||||||
|
|
||||||
|
|
||||||
|
//const xl = x[0];
|
||||||
|
val xh = x[1];
|
||||||
|
//const yl = y[0];
|
||||||
|
val yh = y[1];
|
||||||
|
|
||||||
|
//const [cl1,ch] = twoProduct(xh,yh);
|
||||||
|
val ch = xh * yh;
|
||||||
|
val c = f * xh;
|
||||||
|
val ah = c - (c - xh);
|
||||||
|
val al = xh - ah;
|
||||||
|
val d = f * yh;
|
||||||
|
val bh = d - (d - yh);
|
||||||
|
val bl = yh - bh;
|
||||||
|
val cl1 = (al * bl) - ((ch - (ah * bh)) - (al * bh) - (ah * bl));
|
||||||
|
|
||||||
|
//return fastTwoSum(ch,cl1 + (xh*yl + xl*yh));
|
||||||
|
val b = cl1 + (xh * y[0] + x[0] * yh);
|
||||||
|
val xx = ch + b;
|
||||||
|
|
||||||
|
return doubleArrayOf(b - (xx - ch), xx)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of adding two double-double-precision floating point
|
||||||
|
* numbers.
|
||||||
|
*
|
||||||
|
* * relative error bound: 3u^2 + 13u^3, i.e. fl(a+b) = (a+b)(1+ϵ),
|
||||||
|
* where ϵ <= 3u^2 + 13u^3, u = 0.5 * Number.EPSILON
|
||||||
|
* * the error bound is not sharp - the worst case that could be found by the
|
||||||
|
* authors were 2.25u^2
|
||||||
|
*
|
||||||
|
* ALGORITHM 6 of https://hal.archives-ouvertes.fr/hal-01351529v3/document
|
||||||
|
* @param x a double-double precision floating point number
|
||||||
|
* @param y another double-double precision floating point number
|
||||||
|
*/
|
||||||
|
fun ddAddDd(x: DoubleArray, y: DoubleArray): DoubleArray {
|
||||||
|
val xl = x[0];
|
||||||
|
val xh = x[1];
|
||||||
|
val yl = y[0];
|
||||||
|
val yh = y[1];
|
||||||
|
|
||||||
|
//const [sl,sh] = twoSum(xh,yh);
|
||||||
|
val sh = xh + yh;
|
||||||
|
val _1 = sh - xh;
|
||||||
|
val sl = (xh - (sh - _1)) + (yh - _1);
|
||||||
|
//val [tl,th] = twoSum(xl,yl);
|
||||||
|
val th = xl + yl;
|
||||||
|
val _2 = th - xl;
|
||||||
|
val tl = (xl - (th - _2)) + (yl - _2);
|
||||||
|
val c = sl + th;
|
||||||
|
//val [vl,vh] = fastTwoSum(sh,c)
|
||||||
|
val vh = sh + c;
|
||||||
|
val vl = c - (vh - sh);
|
||||||
|
val w = tl + vl
|
||||||
|
//val [zl,zh] = fastTwoSum(vh,w)
|
||||||
|
val zh = vh + w;
|
||||||
|
val zl = w - (zh - vh);
|
||||||
|
|
||||||
|
return doubleArrayOf(zl, zh)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the product of a double-double-precision floating point number and a
|
||||||
|
* double.
|
||||||
|
*
|
||||||
|
* * slower than ALGORITHM 8 (one call to fastTwoSum more) but about 2x more
|
||||||
|
* accurate
|
||||||
|
* * relative error bound: 1.5u^2 + 4u^3, i.e. fl(a+b) = (a+b)(1+ϵ),
|
||||||
|
* where ϵ <= 1.5u^2 + 4u^3, u = 0.5 * Number.EPSILON
|
||||||
|
* * the bound is very sharp
|
||||||
|
* * probably prefer `ddMultDouble2` due to extra speed
|
||||||
|
*
|
||||||
|
* * ALGORITHM 7 of https://hal.archives-ouvertes.fr/hal-01351529v3/document
|
||||||
|
* @param y a double
|
||||||
|
* @param x a double-double precision floating point number
|
||||||
|
*/
|
||||||
|
fun ddMultDouble1(y: Double, x: DoubleArray): DoubleArray {
|
||||||
|
val xl = x[0];
|
||||||
|
val xh = x[1];
|
||||||
|
|
||||||
|
//val [cl1,ch] = twoProduct(xh,y);
|
||||||
|
val ch = xh * y;
|
||||||
|
val c = f * xh;
|
||||||
|
val ah = c - (c - xh);
|
||||||
|
val al = xh - ah;
|
||||||
|
val d = f * y;
|
||||||
|
val bh = d - (d - y);
|
||||||
|
val bl = y - bh;
|
||||||
|
val cl1 = (al * bl) - ((ch - (ah * bh)) - (al * bh) - (ah * bl));
|
||||||
|
|
||||||
|
val cl2 = xl * y;
|
||||||
|
//val [tl1,th] = fastTwoSum(ch,cl2);
|
||||||
|
val th = ch + cl2;
|
||||||
|
val tl1 = cl2 - (th - ch);
|
||||||
|
|
||||||
|
val tl2 = tl1 + cl1;
|
||||||
|
//val [zl,zh] = fastTwoSum(th,tl2);
|
||||||
|
val zh = th + tl2;
|
||||||
|
val zl = tl2 - (zh - th);
|
||||||
|
|
||||||
|
return doubleArrayOf(zl, zh);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.icegps.triangulation
|
||||||
|
|
||||||
|
fun orient2d(bx: Double, by: Double, ax: Double, ay: Double, cx: Double, cy: Double): Double {
|
||||||
|
// (ax,ay) (bx,by) are swapped such that the sign of the determinant is flipped. which is what Delaunator.kt expects.
|
||||||
|
|
||||||
|
/*
|
||||||
|
| a b | = | ax - cx ay - cy |
|
||||||
|
| c d | | bx - cx by - cy |
|
||||||
|
*/
|
||||||
|
|
||||||
|
val a = twoDiff(ax, cx)
|
||||||
|
val b = twoDiff(ay, cy)
|
||||||
|
val c = twoDiff(bx, cx)
|
||||||
|
val d = twoDiff(by, cy)
|
||||||
|
|
||||||
|
val determinant = ddDiffDd(ddMultDd(a, d), ddMultDd(b, c))
|
||||||
|
|
||||||
|
return determinant[1]
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
## Demos
|
## Demos
|
||||||
### colormap/DemoSpectralZucconiColormap
|
### colormap/DemoSpectralZucconiColormap
|
||||||
|
|
||||||
|
This program demonstrates the `spectralZucconi6()` function, which
|
||||||
|
takes a normalized value and returns a `ColorRGBa` using the
|
||||||
|
accurate spectral colormap developed by Alan Zucconi.
|
||||||
|
|
||||||
|
It draws a varying number of vertical bands (between 16 and 48)
|
||||||
|
filled with various hues.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -59,7 +64,13 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
|
|
||||||
### colormap/DemoSpectralZucconiColormapPhrase
|
### colormap/DemoSpectralZucconiColormapPhrase
|
||||||
|
|
||||||
|
This program demonstrates how to use the shader-based version of
|
||||||
|
the `spectral_zucconi6()` function, which
|
||||||
|
takes a normalized value and returns an `rgb` color using the
|
||||||
|
accurate spectral colormap developed by Alan Zucconi.
|
||||||
|
|
||||||
|
It shades a full-window rectangle using its normalized `x` coordinate
|
||||||
|
in a `ShadeStyle` to choose pixel colors.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -67,7 +78,11 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
|
|
||||||
### colormap/DemoSpectralZucconiColormapPlot
|
### colormap/DemoSpectralZucconiColormapPlot
|
||||||
|
|
||||||
|
This demo uses the shader based `spectral_zucconi6()` function to fill the background,
|
||||||
|
then visualizes the red, green and blue components of the colors used in the background
|
||||||
|
as red, green and blue line strips.
|
||||||
|
|
||||||
|
The Vector2 points for the line strips are calculated only once when the program starts.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -75,7 +90,12 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
|
|
||||||
### colormap/DemoTurboColormap
|
### colormap/DemoTurboColormap
|
||||||
|
|
||||||
|
This program demonstrates the `turboColormap()` function, which
|
||||||
|
takes a normalized value and returns a `ColorRGBa` using the
|
||||||
|
Turbo colormap developed by Google.
|
||||||
|
|
||||||
|
It draws a varying number of vertical bands (between 16 and 48)
|
||||||
|
filled with various hues.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -83,7 +103,13 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
|
|
||||||
### colormap/DemoTurboColormapPhrase
|
### colormap/DemoTurboColormapPhrase
|
||||||
|
|
||||||
|
This program demonstrates how to use the shader-based version of
|
||||||
|
the `turbo_colormap()` function, which
|
||||||
|
takes a normalized value and returns an `rgb` color using the
|
||||||
|
Turbo colormap developed by Google.
|
||||||
|
|
||||||
|
It shades a full-window rectangle using its normalized `x` coordinate
|
||||||
|
in a `ShadeStyle` to choose pixel colors.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -91,7 +117,11 @@ Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an impleme
|
|||||||
|
|
||||||
### colormap/DemoTurboColormapPlot
|
### colormap/DemoTurboColormapPlot
|
||||||
|
|
||||||
|
This demo uses the shader based `turbo_colormap()` function to fill the background,
|
||||||
|
then visualizes the red, green and blue components of the colors used in the background
|
||||||
|
as red, green and blue line strips.
|
||||||
|
|
||||||
|
The Vector2 points for the line strips are calculated only once when the program starts.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -171,7 +201,8 @@ to position the images dynamically based on their index within the grid.
|
|||||||
|
|
||||||
### colorRange/DemoColorRange01
|
### colorRange/DemoColorRange01
|
||||||
|
|
||||||
|
Comparison of color lists generated by interpolating from
|
||||||
|
`PINK` to `BLUE` in six different color spaces.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -179,7 +210,13 @@ to position the images dynamically based on their index within the grid.
|
|||||||
|
|
||||||
### colorRange/DemoColorRange02
|
### colorRange/DemoColorRange02
|
||||||
|
|
||||||
|
Demonstrates how to create a `ColorSequence` containing three colors, one of them in the HSLUV color space.
|
||||||
|
|
||||||
|
Each color in the sequence is assigned a normalized position: in this program, one at the start (0.0),
|
||||||
|
one in the middle (0.5) and one at the end (1.0).
|
||||||
|
|
||||||
|
The `ColorSpace.blend()` method is used to get a list with 18 interpolated `ColorRGBa` colors,
|
||||||
|
then those colors are drawn as vertical rectangles covering the whole window.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -187,7 +224,13 @@ to position the images dynamically based on their index within the grid.
|
|||||||
|
|
||||||
### colorRange/DemoColorRange03
|
### colorRange/DemoColorRange03
|
||||||
|
|
||||||
|
This program creates color interpolations from `ColorRGBa.BLUE` to
|
||||||
|
`ColorRGBa.PINK` in 25 steps in multiple color spaces.
|
||||||
|
|
||||||
|
The window height is adjusted based on the number of interpolations to show.
|
||||||
|
|
||||||
|
The resulting gradients differ in saturation and brightness and apparently include more
|
||||||
|
`BLUE` or more `PINK` depending on the chosen color space.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -195,6 +238,17 @@ to position the images dynamically based on their index within the grid.
|
|||||||
|
|
||||||
### colorRange/DemoColorRange04
|
### colorRange/DemoColorRange04
|
||||||
|
|
||||||
|
A visualization of color interpolations inside a 3D RGB cube with an interactive 3D `Orbital` camera.
|
||||||
|
|
||||||
|
The hues of the source and target colors are animated over time.
|
||||||
|
|
||||||
|
The color interpolations are shown simultaneously in nine different color spaces, revealing how in
|
||||||
|
each case they share common starting and ending points in 3D, but have unique paths going from
|
||||||
|
start to end.
|
||||||
|
|
||||||
|
By rotating the cube 90 degrees towards the left and slightly zooming out, one can appreciate how
|
||||||
|
one of the points moves along the edges of the cube, while the other moves on the edges of a
|
||||||
|
smaller, invisible cube.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -337,13 +391,6 @@ The rendering process includes:
|
|||||||
|
|
||||||
### histogram/DemoHistogram01
|
### histogram/DemoHistogram01
|
||||||
|
|
||||||
package histogram
|
|
||||||
|
|
||||||
import org.openrndr.application
|
|
||||||
import org.openrndr.draw.loadImage
|
|
||||||
import org.openrndr.extra.color.statistics.calculateHistogramRGB
|
|
||||||
|
|
||||||
/*
|
|
||||||
Demonstrates how to generate a palette with the top 32 colors
|
Demonstrates how to generate a palette with the top 32 colors
|
||||||
of a loaded image, sorted by luminosity. The colors are displayed
|
of a loaded image, sorted by luminosity. The colors are displayed
|
||||||
as rectangles overlayed on top of the image.
|
as rectangles overlayed on top of the image.
|
||||||
@@ -354,14 +401,6 @@ as rectangles overlayed on top of the image.
|
|||||||
|
|
||||||
### histogram/DemoHistogram02
|
### histogram/DemoHistogram02
|
||||||
|
|
||||||
package histogram
|
|
||||||
|
|
||||||
import org.openrndr.application
|
|
||||||
import org.openrndr.draw.loadImage
|
|
||||||
import org.openrndr.extra.color.statistics.calculateHistogramRGB
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
/*
|
|
||||||
Show the color histogram of an image using non-uniform weighting,
|
Show the color histogram of an image using non-uniform weighting,
|
||||||
prioritizing bright colors.
|
prioritizing bright colors.
|
||||||
|
|
||||||
@@ -371,13 +410,6 @@ prioritizing bright colors.
|
|||||||
|
|
||||||
### histogram/DemoHistogram03
|
### histogram/DemoHistogram03
|
||||||
|
|
||||||
package histogram
|
|
||||||
|
|
||||||
import org.openrndr.application
|
|
||||||
import org.openrndr.draw.loadImage
|
|
||||||
import org.openrndr.extra.color.statistics.calculateHistogramRGB
|
|
||||||
|
|
||||||
/*
|
|
||||||
Create a simple grid-like composition based on colors sampled from image.
|
Create a simple grid-like composition based on colors sampled from image.
|
||||||
The cells are 32 by 32 pixels in size and are filled with a random sample
|
The cells are 32 by 32 pixels in size and are filled with a random sample
|
||||||
taken from the color histogram of the image.
|
taken from the color histogram of the image.
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package colorRange
|
package colorRange
|
||||||
|
|
||||||
// Comparison of color lists generated by interpolating from
|
|
||||||
// PINK to BLUE in different color models
|
|
||||||
|
|
||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.extra.color.palettes.rangeTo
|
import org.openrndr.extra.color.palettes.rangeTo
|
||||||
@@ -11,6 +8,10 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.math.map
|
import org.openrndr.math.map
|
||||||
import org.openrndr.shape.Rectangle
|
import org.openrndr.shape.Rectangle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparison of color lists generated by interpolating from
|
||||||
|
* `PINK` to `BLUE` in six different color spaces.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package colorRange
|
package colorRange
|
||||||
|
|
||||||
// Create a colorSequence with multiple color models
|
|
||||||
|
|
||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.extra.color.palettes.colorSequence
|
import org.openrndr.extra.color.palettes.colorSequence
|
||||||
import org.openrndr.extra.color.spaces.toHSLUVa
|
import org.openrndr.extra.color.spaces.toHSLUVa
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to create a `ColorSequence` containing three colors, one of them in the HSLUV color space.
|
||||||
|
*
|
||||||
|
* Each color in the sequence is assigned a normalized position: in this program, one at the start (0.0),
|
||||||
|
* one in the middle (0.5) and one at the end (1.0).
|
||||||
|
*
|
||||||
|
* The `ColorSpace.blend()` method is used to get a list with 18 interpolated `ColorRGBa` colors,
|
||||||
|
* then those colors are drawn as vertical rectangles covering the whole window.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -14,14 +21,16 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
program {
|
program {
|
||||||
extend {
|
extend {
|
||||||
val cs = colorSequence(0.0 to ColorRGBa.PINK,
|
val cs = colorSequence(
|
||||||
0.5 to ColorRGBa.BLUE,
|
0.0 to ColorRGBa.PINK,
|
||||||
1.0 to ColorRGBa.PINK.toHSLUVa()) // <-- note this one is in hsluv
|
0.5 to ColorRGBa.BLUE,
|
||||||
|
1.0 to ColorRGBa.PINK.toHSLUVa() // <-- note this color is in HSLUV
|
||||||
|
)
|
||||||
|
|
||||||
for (c in cs blend (width / 40)) {
|
for (c in cs blend (width / 40)) {
|
||||||
drawer.fill = c
|
drawer.fill = c
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
drawer.rectangle(0.0, 0.0, 40.0, height.toDouble())
|
drawer.rectangle(0.0, 0.0, 40.0, height.toDouble())
|
||||||
drawer.translate(40.0, 0.0)
|
drawer.translate(40.0, 0.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,36 +6,43 @@ import org.openrndr.draw.loadFont
|
|||||||
import org.openrndr.extra.color.palettes.rangeTo
|
import org.openrndr.extra.color.palettes.rangeTo
|
||||||
import org.openrndr.extra.color.spaces.*
|
import org.openrndr.extra.color.spaces.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This program creates color interpolations from `ColorRGBa.BLUE` to
|
||||||
|
* `ColorRGBa.PINK` in 25 steps in multiple color spaces.
|
||||||
|
*
|
||||||
|
* The window height is adjusted based on the number of interpolations to show.
|
||||||
|
*
|
||||||
|
* The resulting gradients differ in saturation and brightness and apparently include more
|
||||||
|
* `BLUE` or more `PINK` depending on the chosen color space.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
val colorA = ColorRGBa.BLUE
|
||||||
|
val colorB = ColorRGBa.PINK
|
||||||
|
|
||||||
|
val stepCount = 25
|
||||||
|
|
||||||
|
val allSteps = listOf(
|
||||||
|
"RGB" to (colorA..colorB blend stepCount),
|
||||||
|
"RGB linear" to (colorA.toLinear()..colorB.toLinear() blend stepCount),
|
||||||
|
"HSV" to (colorA..colorB.toHSVa() blend stepCount),
|
||||||
|
"Lab" to (colorA.toLABa()..colorB.toLABa() blend stepCount),
|
||||||
|
"LCh(ab)" to (colorA.toLCHABa()..colorB.toLCHABa() blend stepCount),
|
||||||
|
"OKLab" to (colorA.toOKLABa()..colorB.toOKLABa() blend stepCount),
|
||||||
|
"OKLCh" to (colorA.toOKLCHa()..colorB.toOKLCHa() blend stepCount),
|
||||||
|
"OKHSV" to (colorA.toOKHSVa()..colorB.toOKHSVa() blend stepCount),
|
||||||
|
"OKHSL" to (colorA.toOKHSLa()..colorB.toOKHSLa() blend stepCount),
|
||||||
|
"HSLUV" to (colorA.toHSLUVa()..colorB.toHSLUVa() blend stepCount),
|
||||||
|
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
||||||
|
)
|
||||||
|
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
height = 30 + 50 * 11 // row count
|
height = 30 + 50 * allSteps.size
|
||||||
}
|
}
|
||||||
program {
|
program {
|
||||||
extend {
|
extend {
|
||||||
drawer.clear(ColorRGBa.WHITE)
|
drawer.clear(ColorRGBa.WHITE)
|
||||||
|
|
||||||
val colorA = ColorRGBa.BLUE
|
|
||||||
val colorB = ColorRGBa.PINK
|
|
||||||
|
|
||||||
val stepCount = 25
|
|
||||||
|
|
||||||
val allSteps = listOf(
|
|
||||||
"RGB" to (colorA..colorB blend stepCount),
|
|
||||||
"RGB linear" to (colorA.toLinear()..colorB.toLinear() blend stepCount),
|
|
||||||
"HSV" to (colorA..colorB.toHSVa() blend stepCount),
|
|
||||||
"Lab" to (colorA.toLABa()..colorB.toLABa() blend stepCount),
|
|
||||||
"LCh(ab)" to (colorA.toLCHABa()..colorB.toLCHABa() blend stepCount),
|
|
||||||
"OKLab" to (colorA.toOKLABa()..colorB.toOKLABa() blend stepCount),
|
|
||||||
"OKLCh" to (colorA.toOKLCHa()..colorB.toOKLCHa() blend stepCount),
|
|
||||||
"OKHSV" to (colorA.toOKHSVa()..colorB.toOKHSVa() blend stepCount),
|
|
||||||
"OKHSL" to (colorA.toOKHSLa()..colorB.toOKHSLa() blend stepCount),
|
|
||||||
"HSLUV" to (colorA.toHSLUVa()..colorB.toHSLUVa() blend stepCount),
|
|
||||||
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
|
||||||
)
|
|
||||||
|
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
|
|
||||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
||||||
drawer.translate(20.0, 20.0)
|
drawer.translate(20.0, 20.0)
|
||||||
for ((label, steps) in allSteps) {
|
for ((label, steps) in allSteps) {
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ import org.openrndr.extra.color.spaces.toXSLUVa
|
|||||||
import org.openrndr.extra.meshgenerators.sphereMesh
|
import org.openrndr.extra.meshgenerators.sphereMesh
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visualization of color interpolations inside a 3D RGB cube with an interactive 3D `Orbital` camera.
|
||||||
|
*
|
||||||
|
* The hues of the source and target colors are animated over time.
|
||||||
|
*
|
||||||
|
* The color interpolations are shown simultaneously in nine different color spaces, revealing how in
|
||||||
|
* each case they share common starting and ending points in 3D, but have unique paths going from
|
||||||
|
* start to end.
|
||||||
|
*
|
||||||
|
* By rotating the cube 90 degrees towards the left and slightly zooming out, one can appreciate how
|
||||||
|
* one of the points moves along the edges of the cube, while the other moves on the edges of a
|
||||||
|
* smaller, invisible cube.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -44,9 +58,6 @@ fun main() = application {
|
|||||||
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
||||||
)
|
)
|
||||||
|
|
||||||
drawer.stroke = null
|
|
||||||
|
|
||||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
|
||||||
for ((_, steps) in allSteps) {
|
for ((_, steps) in allSteps) {
|
||||||
for (i in steps.indices) {
|
for (i in steps.indices) {
|
||||||
val srgb = steps[i].toSRGB().clip()
|
val srgb = steps[i].toSRGB().clip()
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import org.openrndr.extra.color.colormaps.spectralZucconi6
|
|||||||
import org.openrndr.extra.noise.fastFloor
|
import org.openrndr.extra.noise.fastFloor
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This program demonstrates the `spectralZucconi6()` function, which
|
||||||
|
* takes a normalized value and returns a `ColorRGBa` using the
|
||||||
|
* accurate spectral colormap developed by Alan Zucconi.
|
||||||
|
*
|
||||||
|
* It draws a varying number of vertical bands (between 16 and 48)
|
||||||
|
* filled with various hues.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -14,12 +22,13 @@ fun main() = application {
|
|||||||
extend {
|
extend {
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
val stripeCount = 32 + (sin(seconds) * 16.0).fastFloor()
|
val stripeCount = 32 + (sin(seconds) * 16.0).fastFloor()
|
||||||
|
val bandWidth = width / stripeCount.toDouble()
|
||||||
repeat(stripeCount) { i ->
|
repeat(stripeCount) { i ->
|
||||||
drawer.fill = spectralZucconi6(i / stripeCount.toDouble())
|
drawer.fill = spectralZucconi6(i / stripeCount.toDouble())
|
||||||
drawer.rectangle(
|
drawer.rectangle(
|
||||||
x = i * width / stripeCount.toDouble(),
|
x = i * bandWidth,
|
||||||
y = 0.0,
|
y = 0.0,
|
||||||
width = width / stripeCount.toDouble(),
|
width = bandWidth,
|
||||||
height = height.toDouble(),
|
height = height.toDouble(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import org.openrndr.draw.shadeStyle
|
|||||||
import org.openrndr.extra.color.colormaps.ColormapPhraseBook
|
import org.openrndr.extra.color.colormaps.ColormapPhraseBook
|
||||||
import org.openrndr.extra.shaderphrases.preprocess
|
import org.openrndr.extra.shaderphrases.preprocess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This program demonstrates how to use the shader-based version of
|
||||||
|
* the `spectral_zucconi6()` function, which
|
||||||
|
* takes a normalized value and returns an `rgb` color using the
|
||||||
|
* accurate spectral colormap developed by Alan Zucconi.
|
||||||
|
*
|
||||||
|
* It shades a full-window rectangle using its normalized `x` coordinate
|
||||||
|
* in a `ShadeStyle` to choose pixel colors.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import org.openrndr.extra.color.colormaps.spectralZucconi6
|
|||||||
import org.openrndr.extra.shaderphrases.preprocess
|
import org.openrndr.extra.shaderphrases.preprocess
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This demo uses the shader based `spectral_zucconi6()` function to fill the background,
|
||||||
|
* then visualizes the red, green and blue components of the colors used in the background
|
||||||
|
* as red, green and blue line strips.
|
||||||
|
*
|
||||||
|
* The Vector2 points for the line strips are calculated only once when the program starts.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -20,14 +27,14 @@ fun main() = application {
|
|||||||
fragmentTransform = "x_fill.rgb = spectral_zucconi6(c_boundsPosition.x);"
|
fragmentTransform = "x_fill.rgb = spectral_zucconi6(c_boundsPosition.x);"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function that expects as an argument a function to convert a ColorRGBa into a Double,
|
||||||
|
// and returns a list of Vector2 coordinates.
|
||||||
fun getColormapPoints(
|
fun getColormapPoints(
|
||||||
block: ColorRGBa.() -> Double
|
block: ColorRGBa.() -> Double
|
||||||
) = List(width) { x ->
|
) = List(width) { x ->
|
||||||
Vector2(
|
Vector2(
|
||||||
x.toDouble(),
|
x.toDouble(),
|
||||||
height.toDouble()
|
(1.0 - block(spectralZucconi6(x / width.toDouble()))) * height
|
||||||
- block(spectralZucconi6(x / width.toDouble()))
|
|
||||||
* height.toDouble()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,11 +46,13 @@ fun main() = application {
|
|||||||
shadeStyle = backgroundStyle
|
shadeStyle = backgroundStyle
|
||||||
rectangle(bounds)
|
rectangle(bounds)
|
||||||
shadeStyle = null
|
shadeStyle = null
|
||||||
strokeWeight = 1.0
|
|
||||||
stroke = ColorRGBa.RED
|
stroke = ColorRGBa.RED
|
||||||
lineStrip(redPoints)
|
lineStrip(redPoints)
|
||||||
|
|
||||||
stroke = ColorRGBa.GREEN
|
stroke = ColorRGBa.GREEN
|
||||||
lineStrip(greenPoints)
|
lineStrip(greenPoints)
|
||||||
|
|
||||||
stroke = ColorRGBa.BLUE
|
stroke = ColorRGBa.BLUE
|
||||||
lineStrip(bluePoints)
|
lineStrip(bluePoints)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import org.openrndr.extra.color.colormaps.turboColormap
|
|||||||
import org.openrndr.extra.noise.fastFloor
|
import org.openrndr.extra.noise.fastFloor
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This program demonstrates the `turboColormap()` function, which
|
||||||
|
* takes a normalized value and returns a `ColorRGBa` using the
|
||||||
|
* Turbo colormap developed by Google.
|
||||||
|
*
|
||||||
|
* It draws a varying number of vertical bands (between 16 and 48)
|
||||||
|
* filled with various hues.
|
||||||
|
*/
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import org.openrndr.draw.shadeStyle
|
|||||||
import org.openrndr.extra.color.colormaps.ColormapPhraseBook
|
import org.openrndr.extra.color.colormaps.ColormapPhraseBook
|
||||||
import org.openrndr.extra.shaderphrases.preprocess
|
import org.openrndr.extra.shaderphrases.preprocess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This program demonstrates how to use the shader-based version of
|
||||||
|
* the `turbo_colormap()` function, which
|
||||||
|
* takes a normalized value and returns an `rgb` color using the
|
||||||
|
* Turbo colormap developed by Google.
|
||||||
|
*
|
||||||
|
* It shades a full-window rectangle using its normalized `x` coordinate
|
||||||
|
* in a `ShadeStyle` to choose pixel colors.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import org.openrndr.extra.color.colormaps.turboColormap
|
|||||||
import org.openrndr.extra.shaderphrases.preprocess
|
import org.openrndr.extra.shaderphrases.preprocess
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This demo uses the shader based `turbo_colormap()` function to fill the background,
|
||||||
|
* then visualizes the red, green and blue components of the colors used in the background
|
||||||
|
* as red, green and blue line strips.
|
||||||
|
*
|
||||||
|
* The Vector2 points for the line strips are calculated only once when the program starts.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -23,10 +30,8 @@ fun main() = application {
|
|||||||
block: ColorRGBa.() -> Double
|
block: ColorRGBa.() -> Double
|
||||||
) = List(width) { x ->
|
) = List(width) { x ->
|
||||||
Vector2(
|
Vector2(
|
||||||
x = x.toDouble(),
|
x.toDouble(),
|
||||||
y = height.toDouble()
|
(1.0 - block(turboColormap(x / width.toDouble()))) * height
|
||||||
- block(turboColormap(x / width.toDouble()))
|
|
||||||
* height.toDouble()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val redPoints = getColormapPoints { r }
|
val redPoints = getColormapPoints { r }
|
||||||
@@ -37,11 +42,13 @@ fun main() = application {
|
|||||||
shadeStyle = backgroundStyle
|
shadeStyle = backgroundStyle
|
||||||
rectangle(bounds)
|
rectangle(bounds)
|
||||||
shadeStyle = null
|
shadeStyle = null
|
||||||
strokeWeight = 1.0
|
|
||||||
stroke = ColorRGBa.RED
|
stroke = ColorRGBa.RED
|
||||||
lineStrip(redPoints)
|
lineStrip(redPoints)
|
||||||
|
|
||||||
stroke = ColorRGBa.GREEN
|
stroke = ColorRGBa.GREEN
|
||||||
lineStrip(greenPoints)
|
lineStrip(greenPoints)
|
||||||
|
|
||||||
stroke = ColorRGBa.BLUE
|
stroke = ColorRGBa.BLUE
|
||||||
lineStrip(bluePoints)
|
lineStrip(bluePoints)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ this addon provides some helper functions to convert them to OPENRNDR types:
|
|||||||
## Demos
|
## Demos
|
||||||
### DemoContours01
|
### DemoContours01
|
||||||
|
|
||||||
|
Demonstrates how to convert a PNG image into `ShapeContour`s using BoofCV.
|
||||||
|
|
||||||
|
Two helper methods help convert data types between BoofCV and OPENRNDR.
|
||||||
|
|
||||||
|
The `ColorBuffer.toGrayF32()` method converts an OPENRNDR `ColorBuffer` to `GrayF32` format,
|
||||||
|
required by BoofCV.
|
||||||
|
|
||||||
|
The `.toShapeContours()` converts BoofCV contours to OPENRNDR `ShapeContour` instances.
|
||||||
|
|
||||||
|
The resulting contours are animated zooming in and out while their colors change slowly.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -34,7 +43,8 @@ this addon provides some helper functions to convert them to OPENRNDR types:
|
|||||||
|
|
||||||
### DemoResize01
|
### DemoResize01
|
||||||
|
|
||||||
|
Demonstrates how to scale down images using the `resizeBy` BoofCV-based
|
||||||
|
method.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -42,7 +52,11 @@ this addon provides some helper functions to convert them to OPENRNDR types:
|
|||||||
|
|
||||||
### DemoResize02
|
### DemoResize02
|
||||||
|
|
||||||
|
Demonstrates how to scale down images using the `resizeTo` BoofCV-based
|
||||||
|
method.
|
||||||
|
|
||||||
|
If only the `newWidth` or the `newHeight` arguments are specified,
|
||||||
|
the resizing happens maintaining the original aspect ratio.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -50,7 +64,16 @@ this addon provides some helper functions to convert them to OPENRNDR types:
|
|||||||
|
|
||||||
### DemoSimplified01
|
### DemoSimplified01
|
||||||
|
|
||||||
|
When converting a `ColorBuffer` to `ShapeContour` instances using
|
||||||
|
`BoofCV`, simple shapes can have hundreds of segments and vertices.
|
||||||
|
|
||||||
|
This demo shows how to use the `simplify()` method to greatly
|
||||||
|
reduce the number of vertices.
|
||||||
|
|
||||||
|
Then it uses the simplified vertex lists to create smooth curves
|
||||||
|
(using `CatmullRomChain2`) and polygonal curves (using `ShapeContour.fromPoints`).
|
||||||
|
|
||||||
|
Study the console to learn about the number of segments before and after simplification.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ import org.openrndr.draw.loadImage
|
|||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to convert a PNG image into `ShapeContour`s using BoofCV.
|
||||||
|
*
|
||||||
|
* Two helper methods help convert data types between BoofCV and OPENRNDR.
|
||||||
|
*
|
||||||
|
* The `ColorBuffer.toGrayF32()` method converts an OPENRNDR `ColorBuffer` to `GrayF32` format,
|
||||||
|
* required by BoofCV.
|
||||||
|
*
|
||||||
|
* The `.toShapeContours()` converts BoofCV contours to OPENRNDR `ShapeContour` instances.
|
||||||
|
*
|
||||||
|
* The resulting contours are animated zooming in and out while their colors change slowly.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
// Load an image, convert to BoofCV format using orx-boofcv
|
// Load an image, convert to BoofCV format using orx-boofcv
|
||||||
|
|||||||
@@ -2,19 +2,31 @@ import org.openrndr.application
|
|||||||
import org.openrndr.boofcv.binding.resizeBy
|
import org.openrndr.boofcv.binding.resizeBy
|
||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.draw.loadImage
|
import org.openrndr.draw.loadImage
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to scale down images using the `resizeBy` BoofCV-based
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
// Load an image, convert to BoofCV format using orx-boofcv
|
|
||||||
val input = loadImage("demo-data/images/image-001.png")
|
val input = loadImage("demo-data/images/image-001.png")
|
||||||
|
|
||||||
val scaled = input.resizeBy(0.5)
|
val scaled = input.resizeBy(0.5)
|
||||||
val scaled2 = input.resizeBy(0.25, convertToGray = true)
|
val scaled2 = input.resizeBy(0.25, convertToGray = true)
|
||||||
val scaled3 = input.resizeBy(0.1)
|
val scaled3 = input.resizeBy(0.1)
|
||||||
|
|
||||||
|
println("${input.width} x ${input.height}")
|
||||||
|
println("${scaled.width} x ${scaled.height}")
|
||||||
|
|
||||||
extend {
|
extend {
|
||||||
drawer.clear(ColorRGBa.BLACK)
|
drawer.clear(ColorRGBa.BLACK)
|
||||||
drawer.translate(0.0, (height - scaled.bounds.height) / 2.0)
|
drawer.translate(0.0, (height - scaled.bounds.height) / 2.0)
|
||||||
|
|
||||||
|
// Display the loaded image to the right of `scaled` matching its size
|
||||||
|
drawer.image(input, scaled.bounds.movedBy(Vector2.UNIT_X * scaled.bounds.width))
|
||||||
|
|
||||||
|
// Display actually scaled down versions of the loaded image
|
||||||
drawer.image(scaled)
|
drawer.image(scaled)
|
||||||
drawer.image(scaled2, scaled.bounds.width, scaled.bounds.height - scaled2.height)
|
drawer.image(scaled2, scaled.bounds.width, scaled.bounds.height - scaled2.height)
|
||||||
drawer.image(scaled3, scaled.bounds.width + scaled2.bounds.width, scaled.bounds.height - scaled3.height)
|
drawer.image(scaled3, scaled.bounds.width + scaled2.bounds.width, scaled.bounds.height - scaled3.height)
|
||||||
|
|||||||
@@ -3,17 +3,29 @@ import org.openrndr.boofcv.binding.resizeTo
|
|||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.draw.loadImage
|
import org.openrndr.draw.loadImage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to scale down images using the `resizeTo` BoofCV-based
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* If only the `newWidth` or the `newHeight` arguments are specified,
|
||||||
|
* the resizing happens maintaining the original aspect ratio.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
// Load an image, convert to BoofCV format using orx-boofcv
|
|
||||||
val input = loadImage("demo-data/images/image-001.png")
|
val input = loadImage("demo-data/images/image-001.png")
|
||||||
|
|
||||||
val scaled = input.resizeTo(input.width / 3)
|
val scaled = input.resizeTo(input.width / 3)
|
||||||
val scaled2 = input.resizeTo(newHeight = input.height / 4, convertToGray = true)
|
val scaled2 = input.resizeTo(newHeight = input.height / 4, convertToGray = true)
|
||||||
val scaled3 = input.resizeTo(input.width / 5, input.height / 5)
|
val scaled3 = input.resizeTo(input.width / 5, input.height / 5)
|
||||||
|
|
||||||
|
println("${input.width} x ${input.height}")
|
||||||
|
println("${scaled.width} x ${scaled.height}")
|
||||||
|
|
||||||
extend {
|
extend {
|
||||||
drawer.clear(ColorRGBa.BLACK)
|
drawer.clear(ColorRGBa.BLACK)
|
||||||
drawer.translate(0.0, (height - scaled.bounds.height) / 2.0)
|
drawer.translate(0.0, (height - scaled.bounds.height) / 2.0)
|
||||||
|
|
||||||
|
// Display actually scaled down versions of the loaded image
|
||||||
drawer.image(scaled)
|
drawer.image(scaled)
|
||||||
drawer.image(scaled2, scaled.bounds.width, scaled.bounds.height - scaled2.height)
|
drawer.image(scaled2, scaled.bounds.width, scaled.bounds.height - scaled2.height)
|
||||||
drawer.image(scaled3, scaled.bounds.width + scaled2.bounds.width, scaled.bounds.height - scaled3.height)
|
drawer.image(scaled3, scaled.bounds.width + scaled2.bounds.width, scaled.bounds.height - scaled3.height)
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.shape.Rectangle
|
import org.openrndr.shape.Rectangle
|
||||||
import org.openrndr.shape.ShapeContour
|
import org.openrndr.shape.ShapeContour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When converting a `ColorBuffer` to `ShapeContour` instances using
|
||||||
|
* `BoofCV`, simple shapes can have hundreds of segments and vertices.
|
||||||
|
*
|
||||||
|
* This demo shows how to use the `simplify()` method to greatly
|
||||||
|
* reduce the number of vertices.
|
||||||
|
*
|
||||||
|
* Then it uses the simplified vertex lists to create smooth curves
|
||||||
|
* (using `CatmullRomChain2`) and polygonal curves (using `ShapeContour.fromPoints`).
|
||||||
|
*
|
||||||
|
* Study the console to learn about the number of segments before and after simplification.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
// Create a buffer where to draw something for boofcv
|
// Create a buffer where to draw something for boofcv
|
||||||
@@ -41,6 +53,7 @@ fun main() = application {
|
|||||||
rectangle(0.0, -200.0, 60.0, 60.0)
|
rectangle(0.0, -200.0, 60.0, 60.0)
|
||||||
circle(0.0, 190.0, 60.0)
|
circle(0.0, 190.0, 60.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the bitmap buffer into ShapeContours
|
// Convert the bitmap buffer into ShapeContours
|
||||||
val vectorized = imageToContours(rt.colorBuffer(0))
|
val vectorized = imageToContours(rt.colorBuffer(0))
|
||||||
|
|
||||||
@@ -73,8 +86,11 @@ fun main() = application {
|
|||||||
extend {
|
extend {
|
||||||
drawer.run {
|
drawer.run {
|
||||||
fill = null // ColorRGBa.PINK.opacify(0.15)
|
fill = null // ColorRGBa.PINK.opacify(0.15)
|
||||||
|
|
||||||
stroke = ColorRGBa.PINK.opacify(0.7)
|
stroke = ColorRGBa.PINK.opacify(0.7)
|
||||||
contours(polygonal)
|
contours(polygonal)
|
||||||
|
|
||||||
|
stroke = ColorRGBa.GREEN.opacify(0.7)
|
||||||
contours(smooth)
|
contours(smooth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,10 @@ import org.openrndr.extra.parameters.DoubleParameter
|
|||||||
## Demos
|
## Demos
|
||||||
### DemoAppearance01
|
### DemoAppearance01
|
||||||
|
|
||||||
A simple demonstration of a GUI for drawing some circles
|
Demonstrates how to customize the appearance of the GUI by using
|
||||||
|
`GUIAppearance()`.
|
||||||
|
|
||||||
|
In this demo, we make the GUI wider (400 pixels) and translucent.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -186,7 +189,7 @@ A simple demonstration of a GUI for drawing some circles
|
|||||||
|
|
||||||
### DemoHide01
|
### DemoHide01
|
||||||
|
|
||||||
A simple demonstration of a GUI for drawing some circles
|
Demonstrates how to hide the GUI when the mouse pointer is outside of it.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -194,15 +197,38 @@ A simple demonstration of a GUI for drawing some circles
|
|||||||
|
|
||||||
### DemoOptions01
|
### DemoOptions01
|
||||||
|
|
||||||
A simple demonstration of a GUI with a drop down menu
|
A simple demonstration of a GUI with a drop-down menu.
|
||||||
|
|
||||||
|
The entries in the drop-down menu are taken from an `enum class`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[source code](src/demo/kotlin/DemoOptions01.kt)
|
[source code](src/demo/kotlin/DemoOptions01.kt)
|
||||||
|
|
||||||
|
### DemoOptions02
|
||||||
|
|
||||||
|
A simple demonstration of a GUI with a drop-down menu.
|
||||||
|
|
||||||
|
The entries in the drop-down menu are taken from an `enum class`.
|
||||||
|
The `enum class` entries contain both a name (used in the drop-down)
|
||||||
|
and a `ColorRGBa` instance (used for rendering).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/demo/kotlin/DemoOptions02.kt)
|
||||||
|
|
||||||
### DemoPath01
|
### DemoPath01
|
||||||
|
|
||||||
|
Demonstrates how to include a button for loading images in a GUI, and how to display
|
||||||
|
the loaded image.
|
||||||
|
|
||||||
|
The program applies the `@PathParameter` annotation to a `String` variable, which gets
|
||||||
|
rendered by the GUI as an image-picker button. Note the allowed file `extensions`.
|
||||||
|
|
||||||
|
This mechanism only updates the `String` containing the path of an image file.
|
||||||
|
|
||||||
|
The `watchingImagePath()` delegate property is used to automatically load an image
|
||||||
|
when its `String` argument changes.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -210,9 +236,11 @@ A simple demonstration of a GUI with a drop down menu
|
|||||||
|
|
||||||
### DemoPresets01
|
### DemoPresets01
|
||||||
|
|
||||||
Shows how to store and retrieve in-memory gui presets.
|
Shows how to store and retrieve in-memory GUI presets,
|
||||||
|
each containing two integer values and two colors.
|
||||||
|
|
||||||
Keyboard controls:
|
Keyboard controls:
|
||||||
[Left Shift] + [0]..[9] => store current gui values to a preset
|
[Left Shift] + [0]..[9] => store current GUI values to a preset
|
||||||
[0]..[9] => recall a preset
|
[0]..[9] => recall a preset
|
||||||
|
|
||||||

|

|
||||||
@@ -221,7 +249,17 @@ Keyboard controls:
|
|||||||
|
|
||||||
### DemoSideCanvas01
|
### DemoSideCanvas01
|
||||||
|
|
||||||
A simple demonstration of a GUI for drawing some circles
|
Demonstrates the `GUI.enableSideCanvas` feature.
|
||||||
|
|
||||||
|
When set to true, the `GUI` provides a `canvas` property where one can draw.
|
||||||
|
The size of this canvas is the window size minus the GUI size.
|
||||||
|
|
||||||
|
That's why if we draw a circle at `drawer.width / 2.0` it is centered
|
||||||
|
on the `canvas`, not on the window.
|
||||||
|
|
||||||
|
This demo sets the window to resizable, so if you resize the window
|
||||||
|
you should see tha the circle stays at the center of the canvas.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -229,7 +267,15 @@ A simple demonstration of a GUI for drawing some circles
|
|||||||
|
|
||||||
### DemoSimple01
|
### DemoSimple01
|
||||||
|
|
||||||
A simple demonstration of a GUI for drawing some circles
|
Demonstrates how to create a simple GUI with 4 inputs:
|
||||||
|
- A `ColorParameter` which creates a color picker.
|
||||||
|
- A `DoubleParameter` to control the radius of a circle.
|
||||||
|
- A `Vector2Parameter` to set the position of that circle.
|
||||||
|
- A `DoubleListParameter` which sets the radii of six circles.
|
||||||
|
|
||||||
|
The demo also shows how to use the variables controlled by the GUI
|
||||||
|
inside the program, so changes to those variables affect
|
||||||
|
the rendering in real time.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -237,6 +283,10 @@ A simple demonstration of a GUI for drawing some circles
|
|||||||
|
|
||||||
### DemoXYParameter
|
### DemoXYParameter
|
||||||
|
|
||||||
|
Demonstrates the use of the `@XYParameter` annotation applied to a `Vector2` variable.
|
||||||
|
|
||||||
|
This annotation creates an interactive XY control in a GUI that can be used to update
|
||||||
|
a `Vector2` variable. In this demo it sets the position of a circle.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple demonstration of a GUI for drawing some circles
|
* Demonstrates how to customize the appearance of the GUI by using
|
||||||
|
* `GUIAppearance()`.
|
||||||
|
*
|
||||||
|
* In this demo, we make the GUI wider (400 pixels) and translucent.
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple demonstration of a GUI for drawing some circles
|
* Demonstrates how to hide the GUI when the mouse pointer is outside of it.
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
@@ -29,7 +29,7 @@ fun main() = application {
|
|||||||
gui.add(settings)
|
gui.add(settings)
|
||||||
extend(gui)
|
extend(gui)
|
||||||
|
|
||||||
// note we can only change the visibility after the extend
|
// note we can only change the visibility after the `extend`
|
||||||
gui.visible = false
|
gui.visible = false
|
||||||
|
|
||||||
extend {
|
extend {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.openrndr.window
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Demonstration of multi window GUI in the manual way
|
* Demonstration of a multi window GUI in the manual way
|
||||||
*/
|
*/
|
||||||
fun main() {
|
fun main() {
|
||||||
// skip this demo on CI
|
// skip this demo on CI
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.openrndr.extra.parameters.DoubleParameter
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Demonstration of multi window GUI using WindowedGUI extension
|
* Demonstration of a multi window GUI using the `WindowedGUI` extension
|
||||||
*/
|
*/
|
||||||
fun main() {
|
fun main() {
|
||||||
// skip this demo on CI
|
// skip this demo on CI
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import org.openrndr.extra.parameters.Description
|
|||||||
import org.openrndr.extra.parameters.OptionParameter
|
import org.openrndr.extra.parameters.OptionParameter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple demonstration of a GUI with a drop down menu
|
* A simple demonstration of a GUI with a drop-down menu.
|
||||||
|
*
|
||||||
|
* The entries in the drop-down menu are taken from an `enum class`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
enum class BackgroundColors {
|
enum class BackgroundColors {
|
||||||
@@ -15,6 +17,10 @@ enum class BackgroundColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 360
|
||||||
|
}
|
||||||
program {
|
program {
|
||||||
val gui = GUI()
|
val gui = GUI()
|
||||||
gui.compartmentsCollapsedByDefault = false
|
gui.compartmentsCollapsedByDefault = false
|
||||||
|
|||||||
43
orx-jvm/orx-gui/src/demo/kotlin/DemoOptions02.kt
Normal file
43
orx-jvm/orx-gui/src/demo/kotlin/DemoOptions02.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.gui.GUI
|
||||||
|
import org.openrndr.extra.parameters.Description
|
||||||
|
import org.openrndr.extra.parameters.OptionParameter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple demonstration of a GUI with a drop-down menu.
|
||||||
|
*
|
||||||
|
* The entries in the drop-down menu are taken from an `enum class`.
|
||||||
|
* The `enum class` entries contain both a name (used in the drop-down)
|
||||||
|
* and a `ColorRGBa` instance (used for rendering).
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum class BackgroundColors2(val color: ColorRGBa) {
|
||||||
|
Pink(ColorRGBa.PINK),
|
||||||
|
Black(ColorRGBa.BLACK),
|
||||||
|
Yellow(ColorRGBa.YELLOW)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 360
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
val gui = GUI()
|
||||||
|
gui.compartmentsCollapsedByDefault = false
|
||||||
|
val settings = @Description("Settings") object {
|
||||||
|
@OptionParameter("Background color")
|
||||||
|
var option = BackgroundColors2.Pink
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.add(settings)
|
||||||
|
extend(gui)
|
||||||
|
gui.onChange { name, value ->
|
||||||
|
println("$name: $value")
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
drawer.clear(settings.option.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,18 @@ import org.openrndr.extra.parameters.Description
|
|||||||
import org.openrndr.extra.parameters.PathParameter
|
import org.openrndr.extra.parameters.PathParameter
|
||||||
import org.openrndr.extra.propertywatchers.watchingImagePath
|
import org.openrndr.extra.propertywatchers.watchingImagePath
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to include a button for loading images in a GUI, and how to display
|
||||||
|
* the loaded image.
|
||||||
|
*
|
||||||
|
* The program applies the `@PathParameter` annotation to a `String` variable, which gets
|
||||||
|
* rendered by the GUI as an image-picker button. Note the allowed file `extensions`.
|
||||||
|
*
|
||||||
|
* This mechanism only updates the `String` containing the path of an image file.
|
||||||
|
*
|
||||||
|
* The `watchingImagePath()` delegate property is used to automatically load an image
|
||||||
|
* when its `String` argument changes.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
val gui = GUI()
|
val gui = GUI()
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ import org.openrndr.extra.parameters.Description
|
|||||||
import org.openrndr.extra.parameters.IntParameter
|
import org.openrndr.extra.parameters.IntParameter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows how to store and retrieve in-memory gui presets.
|
* Shows how to store and retrieve in-memory GUI presets,
|
||||||
|
* each containing two integer values and two colors.
|
||||||
|
*
|
||||||
* Keyboard controls:
|
* Keyboard controls:
|
||||||
* [Left Shift] + [0]..[9] => store current gui values to a preset
|
* [Left Shift] + [0]..[9] => store current GUI values to a preset
|
||||||
* [0]..[9] => recall a preset
|
* [0]..[9] => recall a preset
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 480
|
||||||
|
}
|
||||||
program {
|
program {
|
||||||
val gui = GUI()
|
val gui = GUI()
|
||||||
gui.compartmentsCollapsedByDefault = false
|
gui.compartmentsCollapsedByDefault = false
|
||||||
@@ -43,9 +49,9 @@ fun main() = application {
|
|||||||
// Draw a pattern based on modulo
|
// Draw a pattern based on modulo
|
||||||
for (i in 0 until 100) {
|
for (i in 0 until 100) {
|
||||||
if (i % settings.a == 0 || i % settings.b == 0) {
|
if (i % settings.a == 0 || i % settings.b == 0) {
|
||||||
val x = (i % 10) * 64.0
|
val x = (i % 10) * 72.0
|
||||||
val y = (i / 10) * 48.0
|
val y = (i / 10) * 48.0
|
||||||
drawer.rectangle(x, y, 64.0, 48.0)
|
drawer.rectangle(x, y, 72.0, 48.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,29 @@ import org.openrndr.application
|
|||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.extra.gui.GUI
|
import org.openrndr.extra.gui.GUI
|
||||||
import org.openrndr.extra.gui.GUIAppearance
|
import org.openrndr.extra.gui.GUIAppearance
|
||||||
import org.openrndr.extra.parameters.*
|
import org.openrndr.extra.parameters.ColorParameter
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.extra.parameters.Description
|
||||||
|
import org.openrndr.extra.parameters.DoubleParameter
|
||||||
import org.openrndr.panel.elements.draw
|
import org.openrndr.panel.elements.draw
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple demonstration of a GUI for drawing some circles
|
* Demonstrates the `GUI.enableSideCanvas` feature.
|
||||||
|
*
|
||||||
|
* When set to true, the `GUI` provides a `canvas` property where one can draw.
|
||||||
|
* The size of this canvas is the window size minus the GUI size.
|
||||||
|
*
|
||||||
|
* That's why if we draw a circle at `drawer.width / 2.0` it is centered
|
||||||
|
* on the `canvas`, not on the window.
|
||||||
|
*
|
||||||
|
* This demo sets the window to resizable, so if you resize the window
|
||||||
|
* you should see tha the circle stays at the center of the canvas.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 800
|
width = 720
|
||||||
height = 800
|
height = 720
|
||||||
windowResizable = true
|
windowResizable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,17 +34,11 @@ fun main() = application {
|
|||||||
gui.enableSideCanvas = true
|
gui.enableSideCanvas = true
|
||||||
|
|
||||||
val settings = @Description("Settings") object {
|
val settings = @Description("Settings") object {
|
||||||
@DoubleParameter("radius", 0.0, 100.0)
|
@DoubleParameter("radius", 0.0, 200.0)
|
||||||
var radius = 50.0
|
var radius = 50.0
|
||||||
|
|
||||||
@Vector2Parameter("position", 0.0, 1.0)
|
|
||||||
var position = Vector2(0.6, 0.5)
|
|
||||||
|
|
||||||
@ColorParameter("color")
|
@ColorParameter("color")
|
||||||
var color = ColorRGBa.PINK
|
var color = ColorRGBa.PINK
|
||||||
|
|
||||||
@DoubleListParameter("radii", 5.0, 30.0)
|
|
||||||
var radii = mutableListOf(5.0, 6.0, 8.0, 14.0, 20.0, 30.0)
|
|
||||||
}
|
}
|
||||||
gui.add(settings)
|
gui.add(settings)
|
||||||
extend(gui)
|
extend(gui)
|
||||||
@@ -42,7 +47,7 @@ fun main() = application {
|
|||||||
val width = drawer.width
|
val width = drawer.width
|
||||||
val height = drawer.height
|
val height = drawer.height
|
||||||
drawer.fill = settings.color
|
drawer.fill = settings.color
|
||||||
drawer.circle(width/2.0, height/2.0, 100.0)
|
drawer.circle(width / 2.0, height / 2.0, settings.radius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,22 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple demonstration of a GUI for drawing some circles
|
* Demonstrates how to create a simple GUI with 4 inputs:
|
||||||
|
* - A `ColorParameter` which creates a color picker.
|
||||||
|
* - A `DoubleParameter` to control the radius of a circle.
|
||||||
|
* - A `Vector2Parameter` to set the position of that circle.
|
||||||
|
* - A `DoubleListParameter` which sets the radii of six circles.
|
||||||
|
*
|
||||||
|
* The demo also shows how to use the variables controlled by the GUI
|
||||||
|
* inside the program, so changes to those variables affect
|
||||||
|
* the rendering in real time.
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 450
|
||||||
|
}
|
||||||
program {
|
program {
|
||||||
|
|
||||||
val gui = GUI()
|
val gui = GUI()
|
||||||
gui.compartmentsCollapsedByDefault = false
|
gui.compartmentsCollapsedByDefault = false
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import org.openrndr.extra.parameters.Description
|
|||||||
import org.openrndr.extra.parameters.XYParameter
|
import org.openrndr.extra.parameters.XYParameter
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the use of the `@XYParameter` annotation applied to a `Vector2` variable.
|
||||||
|
*
|
||||||
|
* This annotation creates an interactive XY control in a GUI that can be used to update
|
||||||
|
* a `Vector2` variable. In this demo it sets the position of a circle.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 800
|
width = 800
|
||||||
|
|||||||
@@ -42,7 +42,15 @@ More info about the web client:
|
|||||||
## Demos
|
## Demos
|
||||||
### DemoRabbitControl
|
### DemoRabbitControl
|
||||||
|
|
||||||
|
Demonstrates how to use RabbitControl to create a web-based user interface for your program.
|
||||||
|
|
||||||
|
A `settings` object is created using the same syntax used for `orx-gui`, including
|
||||||
|
annotations for different variable types.
|
||||||
|
|
||||||
|
The program then passes these `settings` to the `RabbitControlServer`. A QR-code is displayed
|
||||||
|
to open the web user interface. A clickable URL is also displayed in the console.
|
||||||
|
|
||||||
|
Once the UI is visible in a web browser we can use it to control the OPENRNDR program.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -50,7 +58,10 @@ More info about the web client:
|
|||||||
|
|
||||||
### DemoRabbitControlManualOverlay
|
### DemoRabbitControlManualOverlay
|
||||||
|
|
||||||
|
Demonstrates how the QR-code pointing at the Rabbit Control web-based user interface
|
||||||
|
can be displayed and hidden manually.
|
||||||
|
|
||||||
|
To display the QR-code overlay in this demo, hold down the HOME key in the keyboard.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -58,6 +69,12 @@ More info about the web client:
|
|||||||
|
|
||||||
### DemoRabbitHole
|
### DemoRabbitHole
|
||||||
|
|
||||||
|
Starts the RabbitControlServer with a `Rabbithole` using the key 'orxtest'.
|
||||||
|
|
||||||
|
`Rabbithole` allows you to access your exposed parameters from Internet
|
||||||
|
connected computers that are not in the same network.
|
||||||
|
|
||||||
|
To use it with this example use 'orxtest' as the tunnel-name in https://rabbithole.rabbitcontrol.cc
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -6,7 +6,17 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.math.Vector4
|
import org.openrndr.math.Vector4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to use RabbitControl to create a web-based user interface for your program.
|
||||||
|
*
|
||||||
|
* A `settings` object is created using the same syntax used for `orx-gui`, including
|
||||||
|
* annotations for different variable types.
|
||||||
|
*
|
||||||
|
* The program then passes these `settings` to the `RabbitControlServer`. A QR-code is displayed
|
||||||
|
* to open the web user interface. A clickable URL is also displayed in the console.
|
||||||
|
*
|
||||||
|
* Once the UI is visible in a web browser we can use it to control the OPENRNDR program.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 800
|
width = 800
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import org.openrndr.color.ColorRGBa
|
|||||||
import org.openrndr.extra.parameters.BooleanParameter
|
import org.openrndr.extra.parameters.BooleanParameter
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how the QR-code pointing at the Rabbit Control web-based user interface
|
||||||
|
* can be displayed and hidden manually.
|
||||||
|
*
|
||||||
|
* To display the QR-code overlay in this demo, hold down the HOME key in the keyboard.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 800
|
width = 800
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.math.Vector4
|
import org.openrndr.math.Vector4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the RabbitControlServer with a `Rabbithole` using the key 'orxtest'.
|
||||||
|
*
|
||||||
|
* `Rabbithole` allows you to access your exposed parameters from Internet
|
||||||
|
* connected computers that are not in the same network.
|
||||||
|
*
|
||||||
|
* To use it with this example use 'orxtest' as the tunnel-name in https://rabbithole.rabbitcontrol.cc
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 800
|
width = 800
|
||||||
@@ -14,13 +22,6 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
program {
|
program {
|
||||||
/**
|
|
||||||
* Start RabbitControlServer with a Rabbithole with key 'orxtest'
|
|
||||||
* Please visit https://rabbithole.rabbitcontrol.cc for more information.
|
|
||||||
*
|
|
||||||
* Rabbithole allows you to access your exposed parameter from the internet.
|
|
||||||
* To use it with this example just use 'orxtest' as tunnel-name on the main page.
|
|
||||||
*/
|
|
||||||
val rabbit = RabbitControlServer(false, 10000, 8080, "wss://rabbithole.rabbitcontrol.cc/public/rcpserver/connect?key=orxtest")
|
val rabbit = RabbitControlServer(false, 10000, 8080, "wss://rabbithole.rabbitcontrol.cc/public/rcpserver/connect?key=orxtest")
|
||||||
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 20.0)
|
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 20.0)
|
||||||
val settings = object {
|
val settings = object {
|
||||||
|
|||||||
@@ -105,7 +105,15 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoAll
|
### DemoAll
|
||||||
|
|
||||||
|
Demonstrates how to create various types of 3D meshes:
|
||||||
|
box, sphere, dodecahedron, cylinder, plane, cap and resolve.
|
||||||
|
|
||||||
|
Two textures are used: one generative with gradients, and the second
|
||||||
|
one is an image loaded from disk. The horizontal mouse position is used
|
||||||
|
to select which of the two textures to use.
|
||||||
|
|
||||||
|
The meshes are positioned in space using a 2D mesh, and displayed
|
||||||
|
rotating on the X and Y axes at different speeds.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -113,6 +121,18 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoBox
|
### DemoBox
|
||||||
|
|
||||||
|
Demonstrates how to create a 3D mesh box by specifying its width, height and depth.
|
||||||
|
|
||||||
|
The `box` is a `VertexBuffer` and contains texture coordinates which can be
|
||||||
|
used to apply a texture to its faces.
|
||||||
|
|
||||||
|
After creating the box, the program creates a texture with a gradient.
|
||||||
|
In it, the red component increases along the x-axis and the green component
|
||||||
|
along the y-axis.
|
||||||
|
|
||||||
|
The scene is rendered with an interactive `Orbital` 3D camera.
|
||||||
|
|
||||||
|
A shade style is used to apply the texture to the box.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -121,7 +141,15 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex01
|
### DemoComplex01
|
||||||
|
|
||||||
|
Demonstrates how to use `buildTriangleMesh` to construct composite 3D meshes.
|
||||||
|
|
||||||
|
A DSL allows specifying the color and transformations of each mesh, in this case,
|
||||||
|
of a sphere and a box.
|
||||||
|
|
||||||
|
An interactive 3D Orbital camera is defined, specifying the location of its `eye` and
|
||||||
|
`lookAt` properties.
|
||||||
|
|
||||||
|
A minimal shade style is used to simulate a uni-directional light pointing along the view Z axis.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -129,7 +157,7 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex02
|
### DemoComplex02
|
||||||
|
|
||||||
|
Demonstrates the creation of a 3D mesh composed of two hemispheres, a cylinder and 12 legs.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -137,7 +165,14 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex03
|
### DemoComplex03
|
||||||
|
|
||||||
|
Demonstrates the creation of a 3D mesh composed of two hemispheres, a cylinder and 12 legs.
|
||||||
|
Additionally, the body of the shape features 5 ridges on the sides
|
||||||
|
of the cylinder.
|
||||||
|
|
||||||
|
The code reveals DSL keywords under `buildTriangleMesh`
|
||||||
|
affecting transformation matrices, for instance `isolated`, `translate` and `rotate`,
|
||||||
|
and mesh generating keywords like
|
||||||
|
`hemisphere`, `taperedCylinder` and `cylinder`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -145,7 +180,9 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex04
|
### DemoComplex04
|
||||||
|
|
||||||
|
Demonstrates the use of `buildTriangleMesh` to create
|
||||||
|
a composite 3D mesh and introduces a new mesh generating keyword:
|
||||||
|
`cap`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -153,6 +190,9 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex05
|
### DemoComplex05
|
||||||
|
|
||||||
|
Demonstrates how to create a 3D grid of extruded shapes
|
||||||
|
(short cylinders), then applies three 3D twists to the
|
||||||
|
composition to deform it.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -161,8 +201,11 @@ Demonstrate decal generation and rendering
|
|||||||
|
|
||||||
### DemoComplex06
|
### DemoComplex06
|
||||||
|
|
||||||
Generates a grid of grids of boxes.
|
Generates a grid of grids of 3D boxes using `buildTriangleMesh` and
|
||||||
Interactive orbital camera.
|
renders them using an interactive orbital camera.
|
||||||
|
|
||||||
|
The cubes ar colorized using a shade style that sets colors based
|
||||||
|
on vertex positions in space, converting XYZ coordinates into RGB colors.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -270,7 +313,13 @@ for a radial-symmetry effect.
|
|||||||
|
|
||||||
### tangents/DemoTangents01
|
### tangents/DemoTangents01
|
||||||
|
|
||||||
|
Tangent and bitangent vectors are used in shader programs for tangent space normal mapping / lighting
|
||||||
|
and certain forms of displacement mapping.
|
||||||
|
|
||||||
|
This demo shows:
|
||||||
|
- how to create a triangulated `MeshData`.
|
||||||
|
- how to estimate the tangents of this MeshData.
|
||||||
|
- How to use the tangent and bitangent attributes in GLSL code.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import org.openrndr.math.Vector2
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.shape.Rectangle
|
import org.openrndr.shape.Rectangle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to create various types of 3D meshes:
|
||||||
|
* box, sphere, dodecahedron, cylinder, plane, cap and resolve.
|
||||||
|
*
|
||||||
|
* Two textures are used: one generative with gradients, and the second
|
||||||
|
* one is an image loaded from disk. The horizontal mouse position is used
|
||||||
|
* to select which of the two textures to use.
|
||||||
|
*
|
||||||
|
* The meshes are positioned in space using a 2D mesh, and displayed
|
||||||
|
* rotating on the X and Y axes at different speeds.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ import org.openrndr.extra.camera.Orbital
|
|||||||
import org.openrndr.extra.meshgenerators.boxMesh
|
import org.openrndr.extra.meshgenerators.boxMesh
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to create a 3D mesh box by specifying its width, height and depth.
|
||||||
|
*
|
||||||
|
* The `box` is a `VertexBuffer` and contains texture coordinates which can be
|
||||||
|
* used to apply a texture to its faces.
|
||||||
|
*
|
||||||
|
* After creating the box, the program creates a texture with a gradient.
|
||||||
|
* In it, the red component increases along the x-axis and the green component
|
||||||
|
* along the y-axis.
|
||||||
|
*
|
||||||
|
* The scene is rendered with an interactive `Orbital` 3D camera.
|
||||||
|
*
|
||||||
|
* A shade style is used to apply the texture to the box.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ import org.openrndr.extra.meshgenerators.buildTriangleMesh
|
|||||||
import org.openrndr.extra.meshgenerators.sphere
|
import org.openrndr.extra.meshgenerators.sphere
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to use `buildTriangleMesh` to construct composite 3D meshes.
|
||||||
|
*
|
||||||
|
* A DSL allows specifying the color and transformations of each mesh, in this case,
|
||||||
|
* of a sphere and a box.
|
||||||
|
*
|
||||||
|
* An interactive 3D Orbital camera is defined, specifying the location of its `eye` and
|
||||||
|
* `lookAt` properties.
|
||||||
|
*
|
||||||
|
* A minimal shade style is used to simulate a uni-directional light pointing along the view Z axis.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import org.openrndr.extra.meshgenerators.cylinder
|
|||||||
import org.openrndr.extra.meshgenerators.hemisphere
|
import org.openrndr.extra.meshgenerators.hemisphere
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the creation of a 3D mesh composed of two hemispheres, a cylinder and 12 legs.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import org.openrndr.extra.camera.Orbital
|
|||||||
import org.openrndr.extra.meshgenerators.*
|
import org.openrndr.extra.meshgenerators.*
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the creation of a 3D mesh composed of two hemispheres, a cylinder and 12 legs.
|
||||||
|
* Additionally, the body of the shape features 5 ridges on the sides
|
||||||
|
* of the cylinder.
|
||||||
|
*
|
||||||
|
* The code reveals DSL keywords under `buildTriangleMesh`
|
||||||
|
* affecting transformation matrices, for instance `isolated`, `translate` and `rotate`,
|
||||||
|
* and mesh generating keywords like
|
||||||
|
* `hemisphere`, `taperedCylinder` and `cylinder`.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import org.openrndr.extra.meshgenerators.*
|
|||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the use of `buildTriangleMesh` to create
|
||||||
|
* a composite 3D mesh and introduces a new mesh generating keyword:
|
||||||
|
* `cap`.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import org.openrndr.extra.meshgenerators.twist
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to create a 3D grid of extruded shapes
|
||||||
|
* (short cylinders), then applies three 3D twists to the
|
||||||
|
* composition to deform it.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import org.openrndr.extra.noise.simplex
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a grid of grids of boxes.
|
* Generates a grid of grids of 3D boxes using `buildTriangleMesh` and
|
||||||
* Interactive orbital camera.
|
* renders them using an interactive orbital camera.
|
||||||
|
*
|
||||||
|
* The cubes ar colorized using a shade style that sets colors based
|
||||||
|
* on vertex positions in space, converting XYZ coordinates into RGB colors.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import org.openrndr.extra.objloader.loadOBJMeshData
|
|||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tangent and bitangent vectors are used in shader programs for tangent space normal mapping / lighting
|
||||||
|
* and certain forms of displacement mapping.
|
||||||
|
*
|
||||||
|
* This demo shows:
|
||||||
|
* - how to create a triangulated `MeshData`.
|
||||||
|
* - how to estimate the tangents of this MeshData.
|
||||||
|
* - How to use the tangent and bitangent attributes in GLSL code.
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
configure {
|
configure {
|
||||||
width = 720
|
width = 720
|
||||||
@@ -29,12 +38,11 @@ fun main() = application {
|
|||||||
fragmentTransform = """
|
fragmentTransform = """
|
||||||
vec3 viewTangent = (u_viewNormalMatrix * u_modelNormalMatrix * vec4(va_tangent, 0.0)).xyz;
|
vec3 viewTangent = (u_viewNormalMatrix * u_modelNormalMatrix * vec4(va_tangent, 0.0)).xyz;
|
||||||
vec3 viewBitangent = (u_viewNormalMatrix * u_modelNormalMatrix * vec4(va_bitangent, 0.0)).xyz;
|
vec3 viewBitangent = (u_viewNormalMatrix * u_modelNormalMatrix * vec4(va_bitangent, 0.0)).xyz;
|
||||||
float c = cos(100.0*dot(v_worldPosition, va_normal)) * 0.5 + 0.5;
|
float c = cos(100.0 * dot(v_worldPosition, va_normal)) * 0.5 + 0.5;
|
||||||
|
|
||||||
//x_fill.rgb = normalize(viewTangent)*0.5+0.5;
|
//x_fill.rgb = normalize(viewTangent) * 0.5 + 0.5;
|
||||||
x_fill.rgb = vec3(c);
|
x_fill.rgb = vec3(c);
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawer.vertexBuffer(objVB, DrawPrimitive.TRIANGLES)
|
drawer.vertexBuffer(objVB, DrawPrimitive.TRIANGLES)
|
||||||
|
|||||||
@@ -465,6 +465,31 @@ to round contours with linear segments.
|
|||||||
|
|
||||||
[source code](src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve03.kt)
|
[source code](src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve03.kt)
|
||||||
|
|
||||||
|
### hobbycurve/DemoHobbyCurve04
|
||||||
|
|
||||||
|
Demonstrates the use of the `tensions` argument when creating a Hobby curve.
|
||||||
|
|
||||||
|
The program starts by creating a random set of scattered points with enough separation between them.
|
||||||
|
The points are sorted using `hilbertOrder` to minimize the travel distance when visiting all the points.
|
||||||
|
Finally, we draw a set of 40 hobby translucent curves using those same points but with varying tensions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve04.kt)
|
||||||
|
|
||||||
|
### hobbycurve/DemoHobbyCurve05
|
||||||
|
|
||||||
|
Demonstrates the creation of a 40 hobby curves with 10 points each.
|
||||||
|
The control points in all hobby curves are almost identical, varying only
|
||||||
|
due to a slight increase in one of the arguments of a simplex noise call.
|
||||||
|
|
||||||
|
The program shows that minor displacements in control points can have
|
||||||
|
a large impact in the resulting curve.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve05.kt)
|
||||||
|
|
||||||
### hobbycurve/DemoHobbyCurve3D01
|
### hobbycurve/DemoHobbyCurve3D01
|
||||||
|
|
||||||
Demonstrates how to use the 3D implementation of the `hobbyCurve` method, to draw a smooth curve passing
|
Demonstrates how to use the 3D implementation of the `hobbyCurve` method, to draw a smooth curve passing
|
||||||
@@ -678,6 +703,22 @@ Demonstrate rectangle-rectangle intersection
|
|||||||
|
|
||||||
[source code](src/jvmDemo/kotlin/primitives/DemoRectangleIntersection01.kt)
|
[source code](src/jvmDemo/kotlin/primitives/DemoRectangleIntersection01.kt)
|
||||||
|
|
||||||
|
### primitives/DemoRectangleIrregularGrid02
|
||||||
|
|
||||||
|
Demonstrates how to use `Rectangle.irregularGrid()` to create a grid with varying column widths
|
||||||
|
and row heights. The widths and heights are specified as a list of 13 `Double` values, each
|
||||||
|
picked randomly between the values 1.0 and 4.0. This produces two types of columns and two
|
||||||
|
types of rows only: wide ones and narrow ones.
|
||||||
|
|
||||||
|
The program also demonstrates how to query a `row()` and a `column()` from a `RectangleGrid` instance,
|
||||||
|
both of which return a `List<Rectangle>`. Both `Rectangle` lists are rendered with translucent
|
||||||
|
colors, which makes the intersection of the column and the row slightly brighter.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/jvmDemo/kotlin/primitives/DemoRectangleIrregularGrid02.kt)
|
||||||
|
|
||||||
### primitives/DemoRectangleIrregularGrid
|
### primitives/DemoRectangleIrregularGrid
|
||||||
|
|
||||||
|
|
||||||
@@ -749,6 +790,21 @@ This serves as a demonstration of positioning and rendering shapes in a structur
|
|||||||
|
|
||||||
[source code](src/jvmDemo/kotlin/primitives/DemoTear01.kt)
|
[source code](src/jvmDemo/kotlin/primitives/DemoTear01.kt)
|
||||||
|
|
||||||
|
### primitives/DemoTear02
|
||||||
|
|
||||||
|
Demonstrates the use of `Tear()` to create drop-like shapes out of a Vector2 point and a Circle.
|
||||||
|
|
||||||
|
The tear locations are calculated using the `Rectangle.scatter()` function. Locations near the
|
||||||
|
center of the window are filtered out.
|
||||||
|
|
||||||
|
The radii of each tear is randomly chosen between three values. The orientation of each tear
|
||||||
|
is calculated by getting the normalized difference between the tear and the center of the window,
|
||||||
|
making them look as being emitted at the center of the window.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/jvmDemo/kotlin/primitives/DemoTear02.kt)
|
||||||
|
|
||||||
### rectify/DemoRectifiedContour01
|
### rectify/DemoRectifiedContour01
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
38
orx-shapes/src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve04.kt
Normal file
38
orx-shapes/src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve04.kt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package hobbycurve
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.noise.scatter
|
||||||
|
import org.openrndr.extra.shapes.hobbycurve.hobbyCurve
|
||||||
|
import org.openrndr.extra.shapes.ordering.hilbertOrder
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the use of the `tensions` argument when creating a Hobby curve.
|
||||||
|
*
|
||||||
|
* The program starts by creating a random set of scattered points with enough separation between them.
|
||||||
|
* The points are sorted using `hilbertOrder` to minimize the travel distance when visiting all the points.
|
||||||
|
* Finally, we draw a set of 40 hobby translucent curves using those same points but with varying tensions.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
extend {
|
||||||
|
for (i in -20..20) {
|
||||||
|
val t = i / 10.0
|
||||||
|
val points = drawer.bounds.offsetEdges(-50.0)
|
||||||
|
.scatter(25.0, random = Random(0))
|
||||||
|
.hilbertOrder()
|
||||||
|
|
||||||
|
drawer.stroke = ColorRGBa.WHITE.opacify(0.5)
|
||||||
|
drawer.fill = null
|
||||||
|
drawer.contour(hobbyCurve(points, closed = false, tensions = { i, inAngle, outAngle ->
|
||||||
|
Pair(t, t)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
orx-shapes/src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve05.kt
Normal file
38
orx-shapes/src/jvmDemo/kotlin/hobbycurve/DemoHobbyCurve05.kt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package hobbycurve
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.color.presets.WHITE_SMOKE
|
||||||
|
import org.openrndr.extra.noise.simplex
|
||||||
|
import org.openrndr.extra.noise.uniform
|
||||||
|
import org.openrndr.extra.shapes.hobbycurve.hobbyCurve
|
||||||
|
import org.openrndr.extra.shapes.ordering.hilbertOrder
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the creation of a 40 hobby curves with 10 points each.
|
||||||
|
* The control points in all hobby curves are almost identical, varying only
|
||||||
|
* due to a slight increase in one of the arguments of a simplex noise call.
|
||||||
|
*
|
||||||
|
* The program shows that minor displacements in control points can have
|
||||||
|
* a large impact in the resulting curve.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
program {
|
||||||
|
val seed = 68040
|
||||||
|
val curves = List(40) { n ->
|
||||||
|
hobbyCurve(List(10) {
|
||||||
|
Vector2(
|
||||||
|
simplex(seed, it * 13.3, n * 0.001) * 300.0 + 320.0,
|
||||||
|
simplex(seed / 2, it * 77.4, n * 0.001) * 300.0 + 240.0
|
||||||
|
)
|
||||||
|
}.hilbertOrder(), true)
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.WHITE_SMOKE)
|
||||||
|
drawer.fill = null
|
||||||
|
drawer.stroke = ColorRGBa.BLACK.opacify(0.3)
|
||||||
|
drawer.contours(curves)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package primitives
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.color.presets.CORAL
|
||||||
|
import org.openrndr.extra.shapes.primitives.column
|
||||||
|
import org.openrndr.extra.shapes.primitives.irregularGrid
|
||||||
|
import org.openrndr.extra.shapes.primitives.row
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates how to use `Rectangle.irregularGrid()` to create a grid with varying column widths
|
||||||
|
* and row heights. The widths and heights are specified as a list of 13 `Double` values, each
|
||||||
|
* picked randomly between the values 1.0 and 4.0. This produces two types of columns and two
|
||||||
|
* types of rows only: wide ones and narrow ones.
|
||||||
|
*
|
||||||
|
* The program also demonstrates how to query a `row()` and a `column()` from a `RectangleGrid` instance,
|
||||||
|
* both of which return a `List<Rectangle>`. Both `Rectangle` lists are rendered with translucent
|
||||||
|
* colors, which makes the intersection of the column and the row slightly brighter.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
extend {
|
||||||
|
val r = Random(100)
|
||||||
|
val grid = drawer.bounds.irregularGrid(
|
||||||
|
List(13) { listOf(1.0, 4.0).random(r) },
|
||||||
|
List(13) { listOf(1.0, 4.0).random(r) }
|
||||||
|
)
|
||||||
|
|
||||||
|
drawer.fill = null
|
||||||
|
drawer.stroke = ColorRGBa.WHITE
|
||||||
|
drawer.rectangles(grid.flatten())
|
||||||
|
|
||||||
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
|
drawer.fill = ColorRGBa.PINK.opacify(0.5)
|
||||||
|
drawer.rectangles(grid.column(2))
|
||||||
|
|
||||||
|
drawer.fill = ColorRGBa.CORAL.opacify(0.5)
|
||||||
|
drawer.rectangles(grid.row(6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
orx-shapes/src/jvmDemo/kotlin/primitives/DemoTear02.kt
Normal file
42
orx-shapes/src/jvmDemo/kotlin/primitives/DemoTear02.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package primitives
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.noise.scatter
|
||||||
|
import org.openrndr.extra.shapes.primitives.Tear
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the use of `Tear()` to create drop-like shapes out of a Vector2 point and a Circle.
|
||||||
|
*
|
||||||
|
* The tear locations are calculated using the `Rectangle.scatter()` function. Locations near the
|
||||||
|
* center of the window are filtered out.
|
||||||
|
*
|
||||||
|
* The radii of each tear is randomly chosen between three values. The orientation of each tear
|
||||||
|
* is calculated by getting the normalized difference between the tear and the center of the window,
|
||||||
|
* making them look as being emitted at the center of the window.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
val points = drawer.bounds.scatter(40.0, distanceToEdge = 80.0).filter {
|
||||||
|
it.distanceTo(drawer.bounds.center) > 80.0
|
||||||
|
}
|
||||||
|
|
||||||
|
val tears = points.map {
|
||||||
|
val radius = listOf(5.0, 10.0, 20.0).random()
|
||||||
|
val offset = (it - drawer.bounds.center).normalized * radius
|
||||||
|
Tear(it - offset, Circle(it + offset, radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.WHITE)
|
||||||
|
drawer.fill = ColorRGBa.PINK
|
||||||
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
|
drawer.contours(tears.map { it.contour })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,11 @@ Note that drawing inside the `repeat` action has no effect. Have a look at the d
|
|||||||
## Demos
|
## Demos
|
||||||
### DemoRepeat01
|
### DemoRepeat01
|
||||||
|
|
||||||
|
A simple demonstration on using the `repeat` method to execute a function
|
||||||
|
at regular intervals.
|
||||||
|
|
||||||
|
Note that drawing inside the repeat action has no effect.
|
||||||
|
See DemoRepeat02.kt to learn how to trigger drawing.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
@@ -45,14 +50,29 @@ Note that drawing inside the `repeat` action has no effect. Have a look at the d
|
|||||||
|
|
||||||
### DemoRepeat02
|
### DemoRepeat02
|
||||||
|
|
||||||
This demonstrates how to combine `repeat {}` with a postponed event to trigger drawing
|
This demonstrates how to combine `repeat {}` with a postponed event to trigger drawing.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[source code](src/demo/kotlin/DemoRepeat02.kt)
|
[source code](src/demo/kotlin/DemoRepeat02.kt)
|
||||||
|
|
||||||
|
### DemoRepeat03
|
||||||
|
|
||||||
|
Shows how a `repeat` block can update a variable used
|
||||||
|
for rendering. In this demo, the `opacity` variable is
|
||||||
|
reduced on every animation frame, and increased to 1.0
|
||||||
|
every 2 seconds, creating a pulsating animation effect.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[source code](src/demo/kotlin/DemoRepeat03.kt)
|
||||||
|
|
||||||
### DemoTimeOut01
|
### DemoTimeOut01
|
||||||
|
|
||||||
|
Demonstrates the `timeOut` function.
|
||||||
|
|
||||||
|
It is similar to the `repeat` function,
|
||||||
|
but it runs only once after the specified delay in seconds.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.extra.timer.repeat
|
import org.openrndr.extra.timer.repeat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple demonstration on using the `repeat` method to execute a function
|
||||||
|
* at regular intervals.
|
||||||
|
*
|
||||||
|
* Note that drawing inside the repeat action has no effect.
|
||||||
|
* See DemoRepeat02.kt to learn how to trigger drawing.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
repeat(2.0) {
|
repeat(2.0) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.openrndr.events.Event
|
|||||||
import org.openrndr.extra.timer.repeat
|
import org.openrndr.extra.timer.repeat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This demonstrates how to combine `repeat {}` with a postponed event to trigger drawing
|
* This demonstrates how to combine `repeat {}` with a postponed event to trigger drawing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
|||||||
25
orx-timer/src/demo/kotlin/DemoRepeat03.kt
Normal file
25
orx-timer/src/demo/kotlin/DemoRepeat03.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.timer.repeat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows how a `repeat` block can update a variable used
|
||||||
|
* for rendering. In this demo, the `opacity` variable is
|
||||||
|
* reduced on every animation frame, and increased to 1.0
|
||||||
|
* every 2 seconds, creating a pulsating animation effect.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
program {
|
||||||
|
var opacity = 0.0
|
||||||
|
repeat(2.0) {
|
||||||
|
opacity = 1.0
|
||||||
|
}
|
||||||
|
extend {
|
||||||
|
drawer.clear(ColorRGBa.PINK)
|
||||||
|
drawer.stroke = ColorRGBa.BLACK.opacify(opacity)
|
||||||
|
drawer.fill = ColorRGBa.WHITE.opacify(opacity)
|
||||||
|
drawer.circle(width / 2.0, height / 2.0, 200.0)
|
||||||
|
opacity *= 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.extra.timer.timeOut
|
import org.openrndr.extra.timer.timeOut
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demonstrates the `timeOut` function.
|
||||||
|
*
|
||||||
|
* It is similar to the `repeat` function,
|
||||||
|
* but it runs only once after the specified delay in seconds.
|
||||||
|
*
|
||||||
|
*/
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
program {
|
program {
|
||||||
timeOut(2.0) {
|
timeOut(2.0) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ The language also holds some tools to manage the position and orientation of the
|
|||||||
## Demos
|
## Demos
|
||||||
### DemoTurtle01
|
### DemoTurtle01
|
||||||
|
|
||||||
/*
|
|
||||||
Drawing a square using the turtle interface.
|
Drawing a square using the turtle interface.
|
||||||
|
|
||||||

|

|
||||||
@@ -44,7 +43,6 @@ Drawing a square using the turtle interface.
|
|||||||
|
|
||||||
### DemoTurtle02
|
### DemoTurtle02
|
||||||
|
|
||||||
/*
|
|
||||||
A simple random walk made using the turtle interface.
|
A simple random walk made using the turtle interface.
|
||||||
|
|
||||||

|

|
||||||
@@ -53,7 +51,6 @@ A simple random walk made using the turtle interface.
|
|||||||
|
|
||||||
### DemoTurtle03
|
### DemoTurtle03
|
||||||
|
|
||||||
/*
|
|
||||||
Drawing shape contours aligned to the turtle's orientation.
|
Drawing shape contours aligned to the turtle's orientation.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -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