Compare commits

...

11 Commits

Author SHA1 Message Date
204c8fd599 添加 SlopeResult 显示的开关 2025-11-26 18:56:31 +08:00
a58486bff0 去除所有 orx 模块 2025-11-26 17:18:50 +08:00
0c90073363 android 去除 orx 相关依赖 2025-11-26 17:00:11 +08:00
2525d30c80 完成了土方量计算 2025-11-26 15:28:39 +08:00
0d15c60606 使用 CatmullRom 生成平滑曲线 2025-11-26 00:23:55 +08:00
ac86ab3976 Merge branch 'master' into terrain 2025-11-25 23:46:49 +08:00
Abe Pazos
3ba0395c16 add demos to README.md 2025-11-23 13:38:34 +00:00
Abe Pazos
10888b0e83 Update CollectScreenShots.kt
Make top comment finding less strict.
Currently some comments start with /* instead of /**, which leads to import and package lines being included in README.md files.
2025-11-23 13:27:20 +00:00
Abe Pazos
6024e62af0 add orx-jvm demos to README.md 2025-11-22 18:16:54 +00:00
Abe Pazos
4af2ed3fed add demos to README.md 2025-11-22 18:16:54 +00:00
Abe Pazos
522627ca51 Add descriptions to demos 2025-11-22 19:08:30 +01:00
1554 changed files with 2131 additions and 123780 deletions

22
.gitignore vendored
View File

@@ -1,13 +1,11 @@
.idea/
.gradle/
out/
target/
build/
*.iml/
gradle.properties
/hs_err_pid*.log
/gui-parameters/
/ShaderError.glsl
/.kotlin
/.lwjgl
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/.kotlin

1
CNAME
View File

@@ -1 +0,0 @@
orx.openrndr.org

View File

@@ -1,162 +0,0 @@
# Contributing to ORX
Thank you for your interest in contributing to ORX :-)
This repository contains the OPENRNDR extras: a growing library of assorted data structures, algorithms and utilities to complement OPENRNDR.
Other repositories you can contribute to are the [core OPENRNDR](https://github.com/openrndr/openrndr/),
the [guide](https://github.com/openrndr/openrndr-guide/) and the [template](https://github.com/openrndr/openrndr-template/).
Please read the [general information about contributing to OPENRNDR](https://github.com/openrndr/openrndr/blob/master/CONTRIBUTING.md).
This document focuses on specific details about the ORX repository.
## How to build ORX
[See the main readme](https://github.com/openrndr/orx/tree/master?tab=readme-ov-file#publish-and-use-local-builds-of-the-library-in-your-applications).
## Overview
There are two types of ORX extras:
- JVM only. Subfolders of `/orx-jvm/`. These run only on Desktop (not in web browsers).
- Multiplatform. Other `/orx-.../` folders. These run both on Desktop and web browsers.
Each orx folder contains a `README.md`, a `build.gradle.kts` file and a `src` folder.
Please explore several orx directories to get a feel for how they look like.
Gradle tasks are used to update the list of ORX'es in the root README.md,
and to update the list of demos in each ORX'es README.md.
## Folder structure (JVM)
```
orx-magic/
├── README.md
├── build.gradle.kts
└── src/
├── main/
│ └── kotlin/
│ └── Magic.kt
└── demo/
└── kotlin/
├── DemoFoo01.kt
└── DemoBar01.kt
```
## Folder structure (multiplatform)
```
orx-magic/
├── README.md
├── build.gradle.kts
└── src/
├── commonMain/kotlin/
│ └── Magic.kt
├── commonTest/kotlin/
├── jsMain/kotlin/
├── jsTest/kotlin/
├── jvmDemo/kotlin/
│ ├── DemoFoo01.kt
│ └── DemoBar01.kt
├── jvmMain/kotlin/
└── jvmTest/kotlin/
```
Note that inside `src` only `commonMain` is required.
## ORX README.md
Assuming you are creating an orx called `magic`, the readme should be formatted as follows:
```
# orx-magic
One or more lines including a short description to display on the root README.md.
One or more lines including a short description to display on the root README.md.
One or more lines including a short description to display on the root README.md.
Main content describing the usage of orx-magic goes here
...
<!-- __demos__ -->
```
1. Start with a markdown header with the name of the orx followed by an empty line.
2. One or more lines with a brief description to show on the root `README.md`, followed by an empty line.
(The `buildMainReadme` Gradle task will extract this description and update the root `README.md`).
3. A detailed description (a guide) of how to use the orx, possibly with code examples in code fences like
````
```kotlin
//code example
```
````
4. If the orx includes demos (more below), running the `CollectScreenShots` Gradle task will append `<!-- __demos__ -->`
to the readme followed by a list of automatically generated screenshots of the demos and links to their source code.
This is specially useful for orx'es that produce graphical output, but less so for orx'es that interface
with hardware (like `orx-midi`).
## ORX build.gradle.kts
ORX `build.gradle.kts` files declare their dependencies and most follow the same structure.
Please explore various build files and find the simplest one that matches your use case.
Note that the JVM ones are somewhat simpler than the multiplatform ones.
The `plugins` section includes either ``org.openrndr.extra.convention.`kotlin-multiplatform` `` or
``org.openrndr.extra.convention.`kotlin-jvm` `` depending on the orx type.
### JVM
The JVM build files declare separate dependencies for the orx itself (`implementation`) and for usage demos
(`demoImplementation`).
See an [example](https://github.com/openrndr/orx/blob/master/orx-jvm/orx-dnk3/build.gradle.kts).
### Multiplatform
The multiplatform build files may have blocks like `commonMain`, `commonTest`, `jvmTest`, `jvmDemo`, etc. to specify the dependencies for each case. See an [example](https://github.com/openrndr/orx/blob/master/orx-color/build.gradle.kts).
## I want to contribute to the documentation
There are various places where you can contribute without writing code. It will be greatly
appreciated by others trying to learn about OPENRNDR.
### Guide
The [guide](https://guide.openrndr.org/) is the first contact with OPENRNDR for most users.
[Learn how to work on the guide](https://github.com/openrndr/openrndr-guide/blob/dev/contributing.md).
### ORX API page
The [ORX API page](https://orx.openrndr.org/) needs some love too. The content is automatically
extracted from comments written in ORX's source code. It goes like this:
1. Fork the [ORX repo](https://github.com/openrndr/orx/), then clone your fork (so you
have a copy on your computer) and get familiar with OPENRNDR and ORX.
2. Find an undocumented section at https://orx.openrndr.org you want to explain.
3. Find the corresponding Kotlin file in your cloned repo and add missing comments. Read about
the [suggested style](https://developers.google.com/style).
4. Generate the API website locally to verify your changes look correct by running the following
command: `./gradlew dokkaGenerate -Dorg.gradle.jvmargs=-Xmx1536M`. This will create the
html documentation under `build/dokka/html/`.
5. Open the `build/dokka/html/index.html` in your web browser. If something looks off
tweak your comments. Note: the sidebar will be empty unless viewed through a web server.
You can launch one by running `python3 -m http.server --bind 127.0.0.1` in the html folder.
7. To continue improving the API go back to step 3, otherwise send a Pull Requests from your fork.
## Demos
ORX'es often include a `jvmDemo` folder. This folder should contain small programs demonstrating
how the ORX can be used. When the build system runs the
[`CollectScreenShots`](buildSrc/src/main/kotlin/CollectScreenShots.kt) task,
the `SingleScreenshot()` extension will be injected into each program found inside the `jvmDemo`
folder, then executed. A PNG screenshot is saved and pushed into the [`media`](https://github.com/openrndr/orx/tree/media) brach. Finally, links to those PNG images are inserted into the README.md file of each ORX,
together with a link to the source code that produced the screenshot.
This serves two purposes: it can be useful for the user to see images of what the ORX can produce,
while it can also be usefu to detect breaking changes (in case the demo fails to run, or produces a
blank image).
## Gradle tasks
* `CollectScreenShots`
* `buildMainReadme`

25
LICENSE
View File

@@ -1,25 +0,0 @@
BSD 2-Clause License
Copyright (c) 2019, OPENRNDR
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

120
README.md
View File

@@ -1,120 +0,0 @@
# ORX (OPENRNDR EXTRA)
[![ORX maven badge](https://img.shields.io/maven-central/v/org.openrndr.extra/orx-noise-jvm?style=flat&color=%23FFC0CB
)](https://mvnrepository.com/artifact/org.openrndr.extra)
A growing library of assorted data structures, algorithms and utilities to
complement [OPENRNDR](https://github.com/openrndr/openrndr).
Multiplatform, unless they deal with hardware or depend on binary libraries. Those are JVM-only.
Find an auto-generated API documentation page at https://orx.openrndr.org/.
## Usage
To make use of these extensions clone the [OPENRNDR template](https://github.com/openrndr/openrndr-template), uncomment the ones you need in its [build.gradle.kts](https://github.com/openrndr/openrndr-template/blob/master/build.gradle.kts) file, and reload Gradle. Cloning this repo is optional but useful to run the demos in each ORX folder, to study the source code, and to contribute to existing or new ORX'es.
<!-- __orxListBegin__ -->
## Multiplatform
| name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | description |
| --- | --- |
| [`orx-camera`](orx-camera/) | 2D and 3D cameras controllable via mouse and keyboard. |
| [`orx-color`](orx-color/) | Color spaces, palettes, histograms, named colors. |
| [`orx-composition`](orx-composition/) | Shape composition library |
| [`orx-compositor`](orx-compositor/) | Toolkit to make composite (layered) images using blend modes and filters. |
| [`orx-delegate-magic`](orx-delegate-magic/) | Collection of magical property delegators. For tracking variable change or interpolate towards the value of a variable. |
| [`orx-easing`](orx-easing/) | Easing functions for smooth animation or non-linear interpolation. |
| [`orx-envelopes`](orx-envelopes/) | ADSR (Attack, Decay, Sustain, Release) envelopes and tools. |
| [`orx-expression-evaluator`](orx-expression-evaluator/) | Tools to evaluate strings containing mathematical expressions. |
| [`orx-expression-evaluator-typed`](orx-expression-evaluator-typed/) | Tools to evaluate strings containing typed mathematical expressions. |
| [`orx-fcurve`](orx-fcurve/) | FCurves are 1 dimensional function curves constructed from 2D bezier functions. They are often used to control a property over time. `x` values don't have any units, but they often represent a duration in seconds. |
| [`orx-fft`](orx-fft/) | Simple forward and inverse FFT routine |
| [`orx-fx`](orx-fx/) | Ready-to-use GPU-based visual effects or filters. Most include [orx-parameters](https://github.com/openrndr/orx/tree/master/orx-parameters) annotations so they can be easily controlled via orx-gui. |
| [`orx-gradient-descent`](orx-gradient-descent/) | Finds equation inputs that output a minimum value: easy to use gradient descent based minimizer. |
| [`orx-hash-grid`](orx-hash-grid/) | 2D space partitioning for fast point queries. |
| [`orx-image-fit`](orx-image-fit/) | Draws an image ensuring it fits or covers the specified `Rectangle`. |
| [`orx-integral-image`](orx-integral-image/) | CPU and GPU-based implementation for integral images (summed area tables) |
| [`orx-interval-tree`](orx-interval-tree/) | For querying a data set containing time segments (start time and end time) when we need all entries containing a specific time value. Useful when creating a timeline. |
| [`orx-jumpflood`](orx-jumpflood/) | Calculates distance or direction fields from an image. GPU accelerated, 2D. Results are provided as an image. |
| [`orx-kdtree`](orx-kdtree/) | Fast search of points closest to the queried point in a data set. 2D, 3D and 4D. |
| [`orx-marching-squares`](orx-marching-squares/) | Tools for extracting contours from functions |
| [`orx-math`](orx-math/) | Mathematical utilities, including complex numbers, linear ranges, simplex ranges, matrices and radial basis functions (RBF). |
| [`orx-mesh-generators`](orx-mesh-generators/) | 3D-mesh generating functions and DSL. |
| [`orx-mesh-noise`](orx-mesh-noise/) | Generate random samples on the surface of a mesh <!-- __demos__ --> ## Demos ### DemoMeshNoise01 |
| [`orx-no-clear`](orx-no-clear/) | Provides the classical "draw-without-clearing-the-screen" functionality. |
| [`orx-noise`](orx-noise/) | Randomness for every type of person: Perlin, uniform, value, simplex, fractal and many other types of noise. |
| [`orx-obj-loader`](orx-obj-loader/) | Simple loader and saver for Wavefront .obj 3D mesh files. |
| [`orx-palette`](orx-palette/) | Collections of color palettes and tools for interacting with them. |
| [`orx-parameters`](orx-parameters/) | Provides annotations and tools for turning Kotlin properties into introspectable parameters. Used by [`orx-gui`](../orx-jvm/orx-gui/README.md) to automatically generate user interfaces. |
| [`orx-property-watchers`](orx-property-watchers/) | Tools for setting up property watcher based pipelines |
| [`orx-quadtree`](orx-quadtree/) | A [Quadtree](https://en.wikipedia.org/wiki/Quadtree) is a spatial partioning tree structure meant to provide fast spatial queries such as nearest points within a range. |
| [`orx-shade-styles`](orx-shade-styles/) | Shader based fills and strokes, including various types of gradient fills. |
| [`orx-shader-phrases`](orx-shader-phrases/) | A library that provides a `#pragma import` statement for shaders. |
| [`orx-shapes`](orx-shapes/) | Collection of 2D shape generators and modifiers. |
| [`orx-svg`](orx-svg/) | SVG reader and writer library. |
| [`orx-temporal-blur`](orx-temporal-blur/) | Post-processing temporal-blur video effect. CPU intense, therefore not intended for use with the `ScreenRecorder` extension or other real-time uses. |
| [`orx-text-on-contour`](orx-text-on-contour/) | Writing texts on contours. |
| [`orx-text-writer`](orx-text-writer/) | Writing texts with layouts |
| [`orx-time-operators`](orx-time-operators/) | A collection of time-sensitive functions aimed at controlling raw data over-time, such as Envelope and LFO. |
| [`orx-timer`](orx-timer/) | Simple timer functionality providing `repeat`, to run code with a given interval and `timeOut`, to run code once after a given delay. |
| [`orx-triangulation`](orx-triangulation/) | **Delaunay** triangulation and **Voronoi** diagrams. |
| [`orx-turtle`](orx-turtle/) | Bezier (`ShapeContour`) backed [turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics). |
| [`orx-view-box`](orx-view-box/) | To create independent views inside one program window. |
## JVM only
| name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | description |
| --- | --- |
| [`orx-axidraw`](orx-jvm/orx-axidraw/) | GUI for configuring and plotting with an Axidraw pen-plotter. |
| [`orx-boofcv`](orx-jvm/orx-boofcv/) | Helper functions to ease working with the BoofCV computer vision library and its data types. |
| [`orx-chataigne`](orx-jvm/orx-chataigne/) | Expose variables to [Chataigne](http://benjamin.kuperberg.fr/chataigne/en) and any other applications that can interface with it. The current implementation makes use of the OSC protocol and supports `Double` and `ColorRGBa`. |
| [`orx-depth-camera-calibrator`](orx-jvm/orx-depth-camera-calibrator/) | Class to help callibrate depth and transformation matrices when using one or more depth cameras. |
| [`orx-dnk3`](orx-jvm/orx-dnk3/) | A scene graph based 3d renderer with support for Gltf based assets |
| [`orx-file-watcher`](orx-jvm/orx-file-watcher/) | Monitor files on disk and auto-reload them if they change. |
| [`orx-git-archiver`](orx-jvm/orx-git-archiver/) | An extension that hooks into `Program.requestAssets` to commit changed code to Git and provide filenames based on the commit hash. |
| [`orx-git-archiver-gradle`](orx-jvm/orx-git-archiver-gradle/) | A Gradle plugin that turns a git history and `screenshots` directory into a markdown file. |
| [`orx-gui`](orx-jvm/orx-gui/) | Automatic UI (sliders, buttons, etc.) generated from annotated classes and properties. Uses `orx-panel` and `orx-parameters`. |
| [`orx-keyframer`](orx-jvm/orx-keyframer/) | Create animated timelines by specifying properties and times in keyframes, then play it back at any speed (even backwards) automatically interpolating properties. Save, load, use mathematical expressions and callbacks. Powerful and highly reusable. |
| [`orx-kinect-v1`](orx-jvm/orx-kinect-v1/) | Support for the Kinect V1 RGB and depth cameras. |
| [`orx-midi`](orx-jvm/orx-midi/) | MIDI support for keyboards and controllers. Send and receive note and control change events. Bind inputs to variables. |
| [`orx-minim`](orx-jvm/orx-minim/) | Simplifies working with the Minim sound library. Provides sound synthesis and analysis. |
| [`orx-olive`](orx-jvm/orx-olive/) | Provides live coding functionality: updates a running OPENRNDR program when you save your changes. |
| [`orx-osc`](orx-jvm/orx-osc/) | Open Sound Control makes it possible to send and receive messages from other OSC enabled programs in the same or a different computer. Used to create multi-application or multi-device software. |
| [`orx-panel`](orx-jvm/orx-panel/) | The OPENRNDR UI toolkit. Provides buttons, sliders, text, a color picker and much more. HTML/CSS-like. |
| [`orx-poisson-fill`](orx-jvm/orx-poisson-fill/) | Post processing effect that fills transparent parts of the image interpolating the edge pixel colors. GPU-based. |
| [`orx-processing`](orx-jvm/orx-processing/) | orx-processing is a module designed to facilitate seamless type conversions between Processing's types and OPENRNDR's types. It provides utilities and methods that allow developers to integrate the two graphics frameworks effectively by bridging the gap between their respective data structures. |
| [`orx-rabbit-control`](orx-jvm/orx-rabbit-control/) | Creates a web-based remote UI to control your OPENRNDR program from a mobile device or a different computer. Alternative to `orx-gui`. |
| [`orx-syphon`](orx-jvm/orx-syphon/) | Send frames to- and from OPENRNDR to other applications in real time using _Syphon_ for Mac. |
| [`orx-video-profiles`](orx-jvm/orx-video-profiles/) | GIF, H265, PNG, Prores, TIFF and Webp `VideoWriterProfile`s for `ScreenRecorder` and `VideoWriter`. |
<!-- __orxListEnd__ -->
# Developer notes
## Publish and use local builds of the library in your applications
First, build and publish [OPENRNDR](https://github.com/openrndr/openrndr) to the local maven repository:
Run (or import in IntelliJ IDEA and edit the run configuration).
```sh
# In openrndr repository
./gradlew publishToMavenLocal snapshot
```
This command will build and publish a snapshot of the next version of the library to your local maven repository.
The exact version will be shown in the console output during the build process.
Now you can run the same command again but for this repository.
```sh
# In orx repository
./gradlew publishToMavenLocal snapshot
```
It will automatically use the locally published snapshot of OPENRNDR for building ORX and will publish ORX to your local
maven repository with the same logic as before.
Once that's done, you can use the local builds of OPENRNDR and ORX in
your [openrndr-template](https://github.com/openrndr/openrndr-template) by specifying the version that was published.
Take a look at the [wiki](https://github.com/openrndr/openrndr/wiki/Building-OPENRNDR-and-ORX) for a more detailed walk-through.

View File

@@ -6,13 +6,13 @@ plugins {
}
android {
namespace = "com.icegps.orx"
namespace = "com.icegps.geotools"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.icegps.orx"
applicationId = "com.icegps.geotools"
minSdk = 28
targetSdk = 36
versionCode = 1
@@ -51,16 +51,10 @@ dependencies {
implementation(libs.androidx.constraintlayout)
implementation(libs.mapbox.maps)
implementation(project(":math"))
implementation(project(":orx-triangulation")) {
exclude(group = "org.openrndr", module = "openrndr-draw")
}
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(project(":icegps-common"))
implementation(project(":icegps-shared"))
implementation(project(":orx-marching-squares"))
implementation(project(":orx-palette")) {
exclude(group = "org.openrndr", module = "openrndr-draw")
}
implementation(project(":icegps-triangulation"))
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.icegps.orx", appContext.packageName)
assertEquals("com.icegps.geotools", appContext.packageName)
}
}

View File

@@ -1,15 +1,22 @@
package com.icegps.orx
package com.icegps.geotools
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.ktx.area
import com.icegps.orx.ktx.toColorInt
import com.icegps.orx.ktx.toMapboxPoint
import com.icegps.orx.ktx.toast
import com.icegps.geotools.catmullrom.CatmullRomChain2
import com.icegps.geotools.ktx.area
import com.icegps.geotools.ktx.toColorInt
import com.icegps.geotools.ktx.toMapboxPoint
import com.icegps.geotools.ktx.toast
import com.icegps.geotools.marchingsquares.ShapeContour
import com.icegps.geotools.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
@@ -26,16 +33,13 @@ 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 com.icegps.orx.triangulation.DelaunayTriangulation3D
import com.icegps.orx.triangulation.Triangle3D
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Rectangle
import org.openrndr.shape.ShapeContour
import kotlin.math.max
class ContoursManager(
@@ -53,7 +57,7 @@ class ContoursManager(
private var contourSize: Int = 6
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
private var cellSize: Double? = 10.0
private val simplePalette = SimplePalette(
val simplePalette = SimplePalette(
range = 0.0..100.0
)
@@ -102,18 +106,21 @@ class ContoursManager(
}
private var isGridVisible: Boolean = true
private var gridModel: GridModel? = null
private var _gridModel = MutableStateFlow<GridModel?>(null)
val gridModel = _gridModel.asStateFlow()
fun setGridVisible(visible: Boolean) {
if (visible != isGridVisible) {
isGridVisible = visible
if (visible) {
if (gridModel != null) mapView.displayGridModel(
grid = gridModel!!,
sourceId = gridSourceId,
layerId = gridLayerId,
palette = simplePalette::palette
)
_gridModel.value?.let { gridModel ->
mapView.displayGridModel(
grid = gridModel,
sourceId = gridSourceId,
layerId = gridLayerId,
palette = simplePalette::palette
)
}
} else {
mapView.mapboxMap.getStyle { style ->
try {
@@ -129,7 +136,7 @@ class ContoursManager(
}
}
private var triangles: List<Triangle3D> = listOf()
private var triangles: List<Triangle> = listOf()
private var isTriangleVisible: Boolean = true
fun setTriangleVisible(visible: Boolean) {
@@ -148,22 +155,24 @@ class ContoursManager(
}
}
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 points = points.map { Vector3(it.x, it.y, it.z) }
val area = points.area
val triangulation = DelaunayTriangulation3D(points)
val triangles: MutableList<Triangle3D> = triangulation.triangles()
val triangulation = DelaunayTriangulation(points)
val triangles = triangulation.triangles()
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
} else {
@@ -174,7 +183,7 @@ class ContoursManager(
delaunator = triangulation,
cellSize = cellSize,
)
this@ContoursManager.gridModel = gridModel
this@ContoursManager._gridModel.value = gridModel
if (isGridVisible) mapView.displayGridModel(
grid = gridModel,
sourceId = gridSourceId,
@@ -182,7 +191,7 @@ class ContoursManager(
palette = simplePalette::palette
)
}
scope.launch(Dispatchers.Default) {
job = scope.launch(Dispatchers.Default) {
val lineFeatures = mutableListOf<List<Feature>>()
val features = zip.mapIndexed { index, range ->
async {
@@ -218,12 +227,12 @@ class ContoursManager(
}
fun findContours(
triangles: MutableList<Triangle3D>,
triangles: List<Triangle>,
range: ClosedFloatingPointRange<Double>,
area: Rectangle,
cellSize: Double
): List<ShapeContour> {
return org.openrndr.extra.marchingsquares.findContours(
return findContours(
f = { v ->
val triangle = triangles.firstOrNull { triangle ->
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
@@ -263,10 +272,10 @@ class ContoursManager(
style.addSource(source)
val layer = lineLayer(layerId, sourceId) {
lineColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
lineWidth(1.0)
lineCap(LineCap.Companion.ROUND)
lineJoin(LineJoin.Companion.ROUND)
lineCap(LineCap.ROUND)
lineJoin(LineJoin.ROUND)
lineOpacity(0.8)
}
style.addLayer(layer)
@@ -287,17 +296,28 @@ class ContoursManager(
style.addSource(source)
val layer = fillLayer(layerId, sourceId) {
fillColor(Expression.Companion.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
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()))
LineString.fromLngLats(
listOf(
segment.start.toMapboxPoint(),
segment.end.toMapboxPoint()
)
)
}.map { lineString ->
Feature.fromGeometry(lineString).apply {
// 将颜色Int转换为十六进制字符串
@@ -311,6 +331,12 @@ class ContoursManager(
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()) {
@@ -343,7 +369,7 @@ class ContoursManager(
}
}
fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
fun isPointInTriangle3D(point: Vector2D, triangle: List<Vector3D>): Boolean {
require(triangle.size == 3) { "三角形必须有3个顶点" }
val (v1, v2, v3) = triangle
@@ -368,15 +394,15 @@ fun isPointInTriangle3D(point: Vector2, triangle: List<Vector3>): Boolean {
* @param triangle 三角形的三个顶点
* @return 三维点 (x, y, z)
*/
fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
fun interpolateHeight(point: Vector2D, triangle: List<Vector3D>): Vector3D {
/**
* 计算点在三角形中的重心坐标
*/
fun calculateBarycentricCoordinates(
point: Vector2,
v1: Vector3,
v2: Vector3,
v3: Vector3
point: Vector2D,
v1: Vector3D,
v2: Vector3D,
v3: Vector3D
): Triple<Double, Double, Double> {
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
@@ -397,5 +423,5 @@ fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
// 使用重心坐标插值z值
val z = alpha * v1.z + beta * v2.z + gamma * v3.z
return Vector3(point.x, point.y, z)
return Vector3D(point.x, point.y, z)
}

View File

@@ -0,0 +1,197 @@
package com.icegps.geotools
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.Vector2D
import com.icegps.geotools.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)
}
}

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.Vector3D

View File

@@ -0,0 +1,144 @@
package com.icegps.geotools
import android.util.Log
import com.icegps.math.geometry.Vector2D
import com.icegps.geotools.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)
}

View File

@@ -0,0 +1,438 @@
package com.icegps.geotools
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)}")
appendLine("填方: ${"%.1f".format(fillVolume)}")
appendLine("净土方: ${"%.1f".format(netVolume)}")
appendLine("挖方面积: ${"%.1f".format(cutArea)}")
appendLine("填方面积: ${"%.1f".format(fillArea)}")
appendLine("总面积:${"%.1f".format(totalArea)}")
}
}
}
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
}
}

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import com.icegps.common.helper.GeoHelper
import com.icegps.math.geometry.Vector2D

View File

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

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
@@ -10,14 +10,22 @@ 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.orx.databinding.ActivityMainBinding
import com.icegps.geotools.databinding.ActivityMainBinding
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.degrees
import com.icegps.shared.model.GeoPoint
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.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
@@ -26,6 +34,7 @@ class MainActivity : AppCompatActivity() {
ViewModelProvider(this)[MainViewModel::class.java]
}
private lateinit var contoursManager: ContoursManager
private lateinit var earthworkManager: EarthworkManager
init {
initGeoHelper()
@@ -36,6 +45,7 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
mapView = binding.mapView
earthworkManager = EarthworkManager(mapView, lifecycleScope)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -52,27 +62,17 @@ class MainActivity : AppCompatActivity() {
.build()
)
val points = coordinateGenerate1()
val points = coordinateGenerate()
val polygonTest = PolygonTest(mapView)
polygonTest.clear()
val innerPoints = points.map { it[0] }
val outerPoints = points.map { it[1] }
if (false) polygonTest.update(
outer = outerPoints,
inner = innerPoints,
other = points.map { it[2] }
)
// divider
contoursManager = ContoursManager(
context = this,
mapView = mapView,
scope = lifecycleScope
)
val points2 = points.flatten()
contoursManager.updateContourSize(6)
contoursManager.updatePoints(points2)
val height = points2.map { it.z }
contoursManager.updatePoints(points)
val height = points.map { it.z }
val min = height.min()
val max = height.max()
contoursManager.updateHeightRange((min / 2)..max)
@@ -125,6 +125,7 @@ class MainActivity : AppCompatActivity() {
override fun onStopTrackingTouch(slider: Slider) {
contoursManager.updateCellSize(slider.value.toDouble())
contoursManager.refresh()
}
}
)
@@ -135,17 +136,123 @@ class MainActivity : AppCompatActivity() {
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()
binding.switchSlopeResult.setOnCheckedChangeListener { _, isChecked ->
slopeResultVisible.value = isChecked
}
initData()
}
private val showDesignHeight = MutableStateFlow(false)
private val slopeResultVisible = MutableStateFlow(false)
@OptIn(ExperimentalUuidApi::class)
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)
val slopeResultSourceId: String = Uuid.random().toString()
val slopeResultLayerId: String = Uuid.random().toString()
combine(
earthworkManager.slopeDirection,
earthworkManager.slopePercentage,
earthworkManager.baseHeightOffset,
contoursManager.gridModel,
showDesignHeight,
slopeResultVisible
) {
Params6(
p1 = it[0] as Angle,
p2 = it[1] as Double,
p3 = it[2] as Double,
p4 = it[3] as? GridModel?,
p5 = it[4] as Boolean,
p6 = it[5] as Boolean
)
}.map { (slopeDirection, slopePercentage, baseHeightOffset, gridModel, showDesignHeight, slopeResultVisible) ->
if (!slopeResultVisible) {
mapView.mapboxMap.getStyle { style ->
style.removeStyleLayer(slopeResultLayerId)
style.removeStyleLayer("${slopeResultLayerId}-outline")
style.removeStyleSource(slopeResultSourceId)
}
} else gridModel?.let { gridModel ->
val slopeResult: SlopeResult = SlopeCalculator.calculateSlope(
grid = gridModel,
slopeDirection = slopeDirection.degrees,
slopePercentage = slopePercentage,
baseHeightOffset = baseHeightOffset
)
mapView.displaySlopeResult(
originalGrid = gridModel,
slopeResult = slopeResult,
sourceId = slopeResultSourceId,
layerId = slopeResultLayerId,
palette = contoursManager.simplePalette::palette,
showDesignHeight = showDesignHeight
)
}
}.launchIn(lifecycleScope)
}
}
data class Params6<
out P1,
out P2,
out P3,
out P4,
out P5,
out P6,
>(
val p1: P1,
val p2: P2,
val p3: P3,
val p4: P4,
val p5: P5,
val p6: P6,
)
val home = GeoPoint(114.476060, 22.771073, 30.897)
fun initGeoHelper(base: GeoPoint = home) {

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import android.app.Application
import android.util.Log
@@ -6,7 +6,7 @@ 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.geotools.ktx.toast
import com.icegps.shared.SharedHttpClient
import com.icegps.shared.SharedJson
import com.icegps.shared.api.OpenElevation

View File

@@ -1,9 +1,9 @@
package com.icegps.orx
package com.icegps.geotools
import android.graphics.Color
import com.icegps.math.geometry.Line3D
import com.icegps.math.geometry.Vector3D
import com.icegps.orx.ktx.toMapboxPoint
import com.icegps.geotools.ktx.toMapboxPoint
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.LineString
@@ -15,7 +15,6 @@ import com.mapbox.maps.extension.style.layers.properties.generated.LineCap
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import org.openrndr.math.YPolarity
class PolylineManager(
private val mapView: MapView
@@ -99,7 +98,6 @@ class PolylineManager(
fun fromPoints(
points: List<Vector3D>,
closed: Boolean,
polarity: YPolarity = YPolarity.CW_NEGATIVE_Y
) = if (points.isEmpty()) {
emptyList()
} else {

View File

@@ -0,0 +1,44 @@
package com.icegps.geotools
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() })
}
}

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import android.util.Log

View File

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

View File

@@ -0,0 +1,427 @@
package com.icegps.geotools.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)

View File

@@ -1,5 +1,5 @@
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import com.icegps.geotools.color.ColorRGBa
import com.icegps.geotools.color.rgb
/**
* # ColorBrewer2

View File

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

View File

@@ -1,4 +1,4 @@
package com.icegps.orx.ktx
package com.icegps.geotools.ktx
import android.content.Context
import android.widget.Toast

View File

@@ -0,0 +1,24 @@
package com.icegps.geotools.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

View File

@@ -1,4 +1,4 @@
package com.icegps.orx.ktx
package com.icegps.geotools.ktx
import com.icegps.common.helper.GeoHelper
import com.icegps.math.geometry.Rectangle

View File

@@ -1,13 +1,72 @@
package org.openrndr.extra.marchingsquares
package com.icegps.geotools.marchingsquares
import org.openrndr.math.IntVector2
import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment
import org.openrndr.shape.Rectangle
import org.openrndr.shape.ShapeContour
import com.icegps.math.geometry.Rectangle
import com.icegps.math.geometry.Vector2D
import com.icegps.math.geometry.Vector2I
import com.icegps.geotools.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.
@@ -18,18 +77,18 @@ import kotlin.math.min
* @return a list of [ShapeContour] instances
*/
fun findContours(
f: (Vector2) -> Double,
f: (Vector2D) -> Double,
area: Rectangle,
cellSize: Double,
useInterpolation: Boolean = true
): List<ShapeContour> {
val segments = mutableListOf<LineSegment>()
val values = mutableMapOf<IntVector2, Double>()
val segmentsMap = mutableMapOf<Vector2, MutableList<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[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
values[Vector2I(x, y)] = f(Vector2D(x * cellSize + area.x, y * cellSize + area.y))
}
}
@@ -39,15 +98,15 @@ fun findContours(
// 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[IntVector2(x, y)] ?: zero)
val v10 = if (y == 0) zero else (values[IntVector2(x + 1, y)] ?: zero)
val v01 = if (x == 0) zero else (values[IntVector2(x, y + 1)] ?: zero)
val v11 = (values[IntVector2(x + 1, y + 1)] ?: zero)
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 = Vector2(x.toDouble(), y.toDouble()) * cellSize + area.corner
val p10 = Vector2((x + 1).toDouble(), y.toDouble()) * cellSize + area.corner
val p01 = Vector2(x.toDouble(), (y + 1).toDouble()) * cellSize + area.corner
val p11 = Vector2((x + 1).toDouble(), (y + 1).toDouble()) * cellSize + area.corner
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) +
@@ -78,8 +137,8 @@ fun findContours(
}
fun emitLine(
p00: Vector2, p01: Vector2, v00: Double, v01: Double,
p10: Vector2, p11: Vector2, v10: Double, v11: Double
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)
@@ -132,10 +191,10 @@ fun findContours(
if (segment in processedSegments) {
continue
} else {
val collected = mutableListOf<Vector2>()
val collected = mutableListOf<Vector2D>()
var current: LineSegment? = segment
var closed = true
var lastVertex = Vector2.INFINITY
var lastVertex = Vector2D.INFINITY
do {
current!!
if (lastVertex.squaredDistanceTo(current.start) > 1E-5) {

View File

@@ -1,123 +0,0 @@
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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
<?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="显示设计面" />
<Switch
android:id="@+id/switch_slope_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:switchPadding="16dp"
android:text="显示计算的坡面" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -103,6 +103,79 @@
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="显示设计面" />
<Switch
android:id="@+id/switch_slope_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:switchPadding="16dp"
android:text="显示计算的坡面" />
</LinearLayout>
<com.mapbox.maps.MapView

View File

@@ -1,4 +1,4 @@
package com.icegps.orx
package com.icegps.geotools
import org.junit.Test

View File

@@ -0,0 +1,71 @@
package com.icegps.geotools
import com.icegps.geotools.ktx.area
import com.icegps.geotools.ktx.niceStr
import com.icegps.math.geometry.Vector3D
import com.icegps.triangulation.delaunayTriangulation
import org.junit.Test
import kotlin.math.max
/**
* @author tabidachinokaze
* @date 2025/11/26
*/
class TriangulationToGridTest {
@Test
fun testTriangulationToGrid() {
val points = listOf(
Vector3D(-10.0, 10.0, 0.0),
Vector3D(10.0, 10.0, 10.0),
Vector3D(-10.0, -10.0, 20.0),
Vector3D(10.0, -10.0, 30.0),
)
points.map {
it / 8
}.niceStr().let(::println)
val area = points.area
val cellSize = max(area.x + area.width, area.y + area.height) / 10
val triangulation = points.delaunayTriangulation()
val triangles = triangulation.triangles()
val grid = triangulationToGrid(
delaunator = triangulation,
cellSize = cellSize,
)
grid.string().let(::println)
val slopeResult = SlopeCalculator.calculateSlope(
grid = grid,
slopeDirection = 0.0,
slopePercentage = 100.0,
baseHeightOffset = 0.0
)
slopeResult.designSurface.string().let(::println)
println("原来的 Volume: ${grid.volumeSum()}")
println("做坡的 Volume: ${slopeResult.designSurface.volumeSum()}")
println(slopeResult.earthworkResult)
}
}
fun GridModel.string() = buildString {
for (r in 0 until rows) {
for (c in 0 until cols) {
val originalElev = getValue(r, c) ?: continue
append("${originalElev.format()}, ")
}
appendLine()
}
}
fun GridModel.volumeSum(): Double {
var volume = 0.0
for (r in 0 until rows) {
for (c in 0 until cols) {
val height = getValue(r, c) ?: continue
volume += height
}
}
return volume
}
fun Double.format(): String {
return "%.1f".format(this)
}

View File

@@ -1,7 +0,0 @@
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}

View File

@@ -1,24 +0,0 @@
plugins {
`kotlin-dsl`
}
val preload: SourceSet by project.sourceSets.creating
repositories {
mavenCentral()
mavenLocal()
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
implementation(project(":orx-variant-plugin"))
implementation(libs.findLibrary("kotlin-gradle-plugin").get())
implementation(libs.findLibrary("dokka-gradle-plugin").get())
"preloadImplementation"(openrndr.application.core)
"preloadImplementation"(openrndr.orextensions)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xskip-metadata-version-check")
}
}
tasks.getByName("compileKotlin").dependsOn("compilePreloadKotlin")

View File

@@ -1,182 +0,0 @@
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileType
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.kotlin.dsl.register
import org.gradle.process.ExecOperations
import org.gradle.work.InputChanges
import java.io.File
import java.net.URLClassLoader
import javax.inject.Inject
private class CustomClassLoader(parent: ClassLoader) : ClassLoader(parent) {
fun findClass(file: File): Class<*> = defineClass(null, file.readBytes(), 0, file.readBytes().size)
}
abstract class CollectScreenshotsTask @Inject constructor() : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:SkipWhenEmpty
abstract val inputDir: DirectoryProperty
@get:InputFiles
abstract val runtimeDependencies: Property<FileCollection>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
@get:Optional
abstract val ignore: ListProperty<String>
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun execute(inputChanges: InputChanges) {
val preloadClass = File(project.rootProject.projectDir, "build-logic/orx-convention/build/classes/kotlin/preload")
require(preloadClass.exists()) {
"preload class not found: '${preloadClass.absolutePath}'"
}
// Execute demos and produce PNG files
inputChanges.getFileChanges(inputDir).forEach { change ->
if (change.fileType == FileType.DIRECTORY) return@forEach
if (change.file.extension == "class") {
var klassName = change.file.nameWithoutExtension
if (klassName.dropLast(2) in ignore.get()) {
return@forEach
}
try {
val cp = (runtimeDependencies.get().map { it.toURI().toURL() } + inputDir.get().asFile.toURI()
.toURL()).toTypedArray()
val ucl = URLClassLoader(cp)
val ccl = CustomClassLoader(ucl)
val tempClass = ccl.findClass(change.file)
klassName = tempClass.name
val klass = ucl.loadClass(klassName)
klass.getMethod("main")
} catch (e: NoSuchMethodException) {
return@forEach
}
println("Collecting screenshot for $klassName")
val imageName = klassName.replace(".", "-")
val pngFile = "${outputDir.get().asFile}/$imageName.png"
fun launchDemoProgram() {
execOperations.javaexec {
this.classpath += project.files(inputDir.get().asFile, preloadClass)
this.classpath += runtimeDependencies.get()
this.mainClass.set(klassName)
this.workingDir(project.rootProject.projectDir)
this.jvmArgs(
"-DtakeScreenshot=true",
"-DscreenshotPath=$pngFile",
"-Dorg.openrndr.exceptions=JVM",
"-Dorg.openrndr.gl3.debug=true",
"-Dorg.openrndr.gl3.delete_angle_on_exit=false"
)
}
}
// A. Create an empty image for quick tests
//File(pngFile).createNewFile()
// B. Create an actual image by running a demo program
runCatching {
launchDemoProgram()
}.onFailure {
println("Retrying $klassName after error: ${it.message}")
Thread.sleep(5000)
launchDemoProgram()
}
}
}
// List produced PNG images.
// Only executed if there are changes in the inputDir.
val demoImageBaseNames = outputDir.get().asFile.listFiles { file: File ->
file.extension == "png"
}!!.sortedBy { it.absolutePath.lowercase() }.map { it.nameWithoutExtension }
// Update readme.md using the found PNG images
val readme = File(project.projectDir, "README.md")
if (readme.exists()) {
var readmeLines = readme.readLines().toMutableList()
val screenshotsLine = readmeLines.indexOfFirst { it == "<!-- __demos__ -->" }
if (screenshotsLine != -1) {
readmeLines = readmeLines.subList(0, screenshotsLine)
}
readmeLines.add("<!-- __demos__ -->")
readmeLines.add("## Demos")
val isKotlinMultiplatform = project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")
val demoModuleName = if (isKotlinMultiplatform) "jvmDemo" else "demo"
for (demoImageBaseName in demoImageBaseNames) {
val projectPath = project.projectDir.relativeTo(project.rootDir)
// val url = "" // for local testing
val url = "https://raw.githubusercontent.com/openrndr/orx/media/$projectPath/"
val imagePath = demoImageBaseName.dropLast(2).replace("-", "/")
val ktFilePath = "src/$demoModuleName/kotlin/$imagePath.kt"
val ktFile = File("$projectPath/$ktFilePath")
val description = if (ktFile.isFile) {
val codeLines = ktFile.readLines()
val main = codeLines.indexOfFirst { it.startsWith("fun main") }
val head = codeLines.take(main)
val start = head.indexOfLast { it.startsWith("/**") }
val end = head.indexOfLast { it.endsWith("*/") }
if ((start < end) && (end < main)) {
codeLines.subList(start + 1, end).joinToString("\n") { line ->
val trimmed = line.trimStart(' ', '*')
if(trimmed.startsWith("@see")) "" else trimmed
}
} else {
println("/** comment */ missing in $projectPath/$ktFilePath")
""
}
} else ""
readmeLines.add(
"""
|### $imagePath
|
|$description
|
|![$demoImageBaseName](${url}images/$demoImageBaseName.png)
|
|[source code]($ktFilePath)
|
""".trimMargin()
)
}
readme.delete()
readme.writeText(readmeLines.joinToString("\n"))
}
}
}
object ScreenshotsHelper {
fun collectScreenshots(
project: Project,
sourceSet: SourceSet,
config: CollectScreenshotsTask.() -> Unit
): CollectScreenshotsTask {
val task = project.tasks.register<CollectScreenshotsTask>("collectScreenshots").get()
task.outputDir.set(project.file(project.projectDir.toString() + "/images"))
task.inputDir.set(File(project.layout.buildDirectory.get().asFile, "classes/kotlin/${sourceSet.name}"))
task.runtimeDependencies.set(sourceSet.runtimeClasspath)
task.config()
task.dependsOn(sourceSet.output)
return task
}
}

View File

@@ -1,67 +0,0 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileType
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject
abstract class EmbedShadersTask : DefaultTask() {
@get:Incremental
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputDirectory
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
abstract val defaultPackage: Property<String>
@get:Input
abstract val defaultVisibility: Property<String>
@get:Input
abstract val namePrefix: Property<String>
@Inject
abstract fun getWorkerExecutor(): WorkerExecutor
init {
defaultVisibility.set("")
namePrefix.set("")
}
@TaskAction
fun execute(inputChanges: InputChanges) {
inputChanges.getFileChanges(inputDir).forEach { change ->
if (change.fileType == FileType.DIRECTORY) return@forEach
val name = "${namePrefix.get()}${change.file.nameWithoutExtension.replace("-", "_")}"
val targetFile = outputDir.file(change.normalizedPath.replace(".", "_") + ".kt").get().asFile
if (change.changeType == ChangeType.REMOVED) {
targetFile.delete()
} else {
val contents = change.file.readText()
val lines = contents.split("\n")
var packageStatement = "package ${defaultPackage.get()}\n"
val visibilityStatement =
if (defaultVisibility.get().isNotBlank()) "${defaultVisibility.get()} " else ""
val r = Regex("#pragma package ([a-z.]+)")
for (line in lines) {
val m = r.find(line.trim())
if (m != null) {
packageStatement = "package ${m.groupValues[1]}\n"
}
}
val text =
"${packageStatement}${visibilityStatement}const val $name = ${"\"\"\""}${contents}${"\"\"\""}"
targetFile.writeText(text)
}
}
}
}

View File

@@ -1,21 +0,0 @@
package org.openrndr.extra.convention
import org.gradle.api.Project
import org.gradle.kotlin.dsl.named
import org.gradle.nativeplatform.MachineArchitecture
import org.gradle.nativeplatform.OperatingSystemFamily
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
val currentOperatingSystemName: String = DefaultNativePlatform.getCurrentOperatingSystem().toFamilyName()
val currentArchitectureName: String = DefaultNativePlatform.getCurrentArchitecture().name
fun Project.addHostMachineAttributesToRuntimeConfigurations() {
configurations.matching {
it.name.endsWith("runtimeClasspath", ignoreCase = true)
}.configureEach {
attributes {
attribute(OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, objects.named(currentOperatingSystemName))
attribute(MachineArchitecture.ARCHITECTURE_ATTRIBUTE, objects.named(currentArchitectureName))
}
}
}

View File

@@ -1,4 +0,0 @@
package org.openrndr.extra.convention
addHostMachineAttributesToRuntimeConfigurations()

View File

@@ -1,38 +0,0 @@
package org.openrndr.extra.convention
plugins {
id("org.jetbrains.dokka")
}
repositories {
mavenCentral()
}
dokka {
pluginsConfiguration.html {
customStyleSheets.from(rootProject.file("dokka/styles/extra.css"))
customAssets.from(rootProject.file("dokka/images/logo-icon.svg"))
}
dokkaSourceSets.configureEach {
skipDeprecated.set(false)
val sourcesDirectory = try {
file("src/$name/kotlin", PathValidation.EXISTS)
} catch (_: InvalidUserDataException) {
return@configureEach
}
// Specifies the location of the project source code on the Web.
// If provided, Dokka generates "source" links for each declaration.
sourceLink {
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
localDirectory = sourcesDirectory
// URL showing where the source code can be accessed through the web browser
remoteUrl("https://github.com/openrndr/orx/blob/master/${moduleName.get()}/src/$name/kotlin")
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}

View File

@@ -1,153 +0,0 @@
package org.openrndr.extra.convention
import ScreenshotsHelper.collectScreenshots
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
val sharedLibs = extensions.getByType(VersionCatalogsExtension::class.java).named("sharedLibs")
val openrndr = extensions.getByType(VersionCatalogsExtension::class.java).named("openrndr")
val libs = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
val shouldPublish = project.name !in setOf("openrndr-demos", "orx-git-archiver-gradle")
plugins {
java
kotlin("jvm")
`maven-publish` apply false
id("org.openrndr.extra.convention.component-metadata-rule")
id("org.openrndr.extra.convention.dokka")
signing
}
if (shouldPublish) {
apply(plugin = "maven-publish")
}
repositories {
mavenCentral()
mavenLocal()
}
group = "org.openrndr.extra"
val main: SourceSet by project.sourceSets.getting
@Suppress("UNUSED_VARIABLE")
val demo: SourceSet by project.sourceSets.creating {
val skipDemos = setOf(
"openrndr-demos",
"orx-axidraw",
"orx-midi",
"orx-minim",
"orx-realsense2",
"orx-runway",
"orx-syphon",
"orx-video-profiles",
"orx-crash-handler"
)
if (project.name !in skipDemos) {
collectScreenshots(project, this@creating) { }
}
}
dependencies {
implementation(sharedLibs.findLibrary("kotlin-stdlib").get())
implementation(sharedLibs.findLibrary("kotlin-logging").get())
testImplementation(sharedLibs.findLibrary("kotlin-test").get())
testRuntimeOnly(sharedLibs.findLibrary("slf4j-simple").get())
"demoImplementation"(main.output.classesDirs + main.runtimeClasspath)
"demoImplementation"(openrndr.findLibrary("application-core").get())
"demoImplementation"(openrndr.findLibrary("orextensions").get())
"demoRuntimeOnly"(openrndr.findLibrary("application-glfw").get())
"demoRuntimeOnly"(sharedLibs.findLibrary("slf4j-simple").get())
}
tasks {
@Suppress("UNUSED_VARIABLE")
val test by getting(Test::class) {
if (DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) {
allJvmArgs = allJvmArgs + "-XstartOnFirstThread"
}
useJUnitPlatform()
testLogging.exceptionFormat = TestExceptionFormat.FULL
}
@Suppress("UNUSED_VARIABLE")
val javadoc by getting(Javadoc::class) {
options {
this as StandardJavadocDocletOptions
addBooleanOption("Xdoclint:none", true)
}
}
withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.valueOf("JVM_${libs.findVersion("jvmTarget").get().displayName.replace(".", "_")}"))
freeCompilerArgs.add("-Xexpect-actual-classes")
freeCompilerArgs.add("-Xjdk-release=${libs.findVersion("jvmTarget").get().displayName}")
apiVersion.set(KotlinVersion.valueOf("KOTLIN_${libs.findVersion("kotlinApi").get().displayName.replace(".", "_")}"))
languageVersion.set(KotlinVersion.valueOf("KOTLIN_${libs.findVersion("kotlinLanguage").get().displayName.replace(".", "_")}"))
}
}
}
java {
withJavadocJar()
withSourcesJar()
targetCompatibility = JavaVersion.valueOf("VERSION_${libs.findVersion("jvmTarget").get().displayName}")
sourceCompatibility = JavaVersion.valueOf("VERSION_${libs.findVersion("jvmTarget").get().displayName}")
}
val isReleaseVersion = !(version.toString()).endsWith("SNAPSHOT")
if (shouldPublish) {
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
groupId = "org.openrndr.extra"
artifactId = project.name
description = project.name
versionMapping {
allVariants {
fromResolutionResult()
}
}
pom {
name.set(project.name)
description.set(project.name)
url.set("https://openrndr.org")
developers {
developer {
id.set("edwinjakobs")
name.set("Edwin Jakobs")
email.set("edwin@openrndr.org")
}
}
licenses {
license {
name.set("BSD-2-Clause")
url.set("https://github.com/openrndr/orx/blob/master/LICENSE")
distribution.set("repo")
}
}
scm {
connection.set("scm:git:git@github.com:openrndr/orx.git")
developerConnection.set("scm:git:ssh://github.com/openrndr/orx.git")
url.set("https://github.com/openrndr/orx")
}
}
}
}
}
signing {
setRequired({ isReleaseVersion && gradle.taskGraph.hasTask("publish") })
sign(publishing.publications)
}
}

View File

@@ -1,198 +0,0 @@
package org.openrndr.extra.convention
import CollectScreenshotsTask
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
val libs = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
val sharedLibs = extensions.getByType(VersionCatalogsExtension::class.java).named("sharedLibs")
val openrndr = extensions.getByType(VersionCatalogsExtension::class.java).named("openrndr")
val shouldPublish = project.name !in setOf("openrndr-demos")
plugins {
kotlin("multiplatform")
`maven-publish` apply false
id("org.openrndr.extra.convention.component-metadata-rule")
id("org.openrndr.extra.convention.dokka")
signing
}
if (shouldPublish) {
apply(plugin = "maven-publish")
}
repositories {
mavenCentral()
mavenLocal()
}
group = "org.openrndr.extra"
tasks.withType<KotlinCompilationTask<*>> {
compilerOptions {
apiVersion.set(KotlinVersion.valueOf("KOTLIN_${libs.findVersion("kotlinApi").get().displayName.replace(".", "_")}"))
languageVersion.set(KotlinVersion.valueOf("KOTLIN_${libs.findVersion("kotlinLanguage").get().displayName.replace(".", "_")}"))
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
tasks.withType<KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.findVersion("jvmTarget").get().displayName))
freeCompilerArgs.add("-Xjdk-release=${libs.findVersion("jvmTarget").get().displayName}")
}
}
kotlin {
jvm {
compilations {
val main by getting
val demo by creating {
associateWith(main)
tasks.register<CollectScreenshotsTask>("collectScreenshots") {
// since Kotlin 2.1.20 output.classesDirs no longer contains a single file
inputDir.set(output.classesDirs.filter { it.path.contains("classes/kotlin") }.singleFile)
runtimeDependencies.set(runtimeDependencyFiles)
outputDir.set(project.file(project.projectDir.toString() + "/images"))
dependsOn(compileTaskProvider)
}
dependencies {
runtimeOnly(openrndr.findLibrary("application-glfw").get())
}
}
}
testRuns["test"].executionTask {
useJUnitPlatform()
testLogging.exceptionFormat = TestExceptionFormat.FULL
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
mainRun {
classpath(kotlin.jvm().compilations.getByName("demo").output.allOutputs)
classpath(kotlin.jvm().compilations.getByName("demo").configurations.runtimeDependencyConfiguration!!)
}
}
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.findLibrary("kotlin-stdlib").get())
implementation(sharedLibs.findLibrary("kotlin-logging").get())
}
}
val commonTest by getting {
dependencies {
implementation(libs.findLibrary("kotlin-test").get())
}
}
val jvmTest by getting {
dependencies {
runtimeOnly(sharedLibs.findBundle("jupiter").get())
runtimeOnly(sharedLibs.findLibrary("slf4j.simple").get())
}
}
val jvmDemo by getting {
dependencies {
implementation(openrndr.findLibrary("application-core").get())
implementation(openrndr.findLibrary("orextensions").get())
runtimeOnly(openrndr.findLibrary("application-glfw").get())
runtimeOnly(sharedLibs.findLibrary("slf4j-simple").get())
}
}
}
}
val isReleaseVersion = !(version.toString()).endsWith("SNAPSHOT")
if (shouldPublish) {
publishing {
publications {
val fjdj = tasks.register("fakeJavaDocJar", Jar::class) {
archiveClassifier.set("javadoc")
}
named("js") {
this as MavenPublication
versionMapping {
allVariants {
fromResolutionOf("jsMainResolvableDependenciesMetadata")
}
}
}
named("jvm") {
this as MavenPublication
this.artifact(fjdj)
versionMapping {
allVariants {
fromResolutionOf("jvmMainResolvableDependenciesMetadata")
}
}
}
named("kotlinMultiplatform") {
this as MavenPublication
versionMapping {
allVariants {
fromResolutionOf("commonMainResolvableDependenciesMetadata")
}
}
}
all {
this as MavenPublication
pom {
name.set(project.name)
description.set(project.name)
url.set("https://openrndr.org")
developers {
developer {
id.set("edwinjakobs")
name.set("Edwin Jakobs")
email.set("edwin@openrndr.org")
}
}
licenses {
license {
name.set("BSD-2-Clause")
url.set("https://github.com/openrndr/orx/blob/master/LICENSE")
distribution.set("repo")
}
}
scm {
connection.set("scm:git:git@github.com:openrndr/orx.git")
developerConnection.set("scm:git:ssh://github.com/openrndr/orx.git")
url.set("https://github.com/openrndr/orx")
}
}
}
}
}
signing {
setRequired({ isReleaseVersion && gradle.taskGraph.hasTask("publish") })
sign(publishing.publications)
}
}
tasks.withType<JavaExec>().matching { it.name == "jvmRun" }.configureEach {
workingDir = rootDir
val os: OperatingSystem? = DefaultNativePlatform.getCurrentOperatingSystem()
if (os?.name == "Mac OS X") {
setJvmArgs(listOf("-XstartOnFirstThread"))
}
}

View File

@@ -1,5 +0,0 @@
package org.openrndr.extra.convention
plugins {
id("orx-variant")
}

View File

@@ -1,14 +0,0 @@
package org.openrndr
import org.openrndr.extensions.SingleScreenshot
/**
* This [Preload] class is used by the [CollectScreenshots] task to inject the [SingleScreenshot] extension
*/
class Preload : ApplicationPreload() {
override fun onProgramSetup(program: Program) {
program.extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
}

View File

@@ -1,17 +0,0 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
create("orxVariants") {
id = "orx-variant"
implementationClass = "org.openrndr.extra.variant.plugin.VariantPlugin"
}
}
}

View File

@@ -1,169 +0,0 @@
package org.openrndr.extra.variant.plugin
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.component.AdhocComponentWithVariants
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.jvm.JvmComponentDependencies
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskContainer
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.named
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import javax.inject.Inject
fun arch(arch: String = System.getProperty("os.arch")): String {
return when (arch) {
"x86-64", "x86_64", "amd64" -> "x86-64"
"arm64", "aarch64" -> "aarch64"
else -> error("unsupported arch $arch")
}
}
abstract class VariantContainer @Inject constructor(
@Inject val tasks: TaskContainer,
val apiElements: Configuration,
val runtimeElements: Configuration,
val sourceSet: SourceSet
) {
@Nested
abstract fun getDependencies(): JvmComponentDependencies
fun Dependency.withClassifier(classifier: String): String {
return "$group:$name:$version:$classifier"
}
/**
* Setup dependencies for this variant.
*/
fun dependencies(action: Action<in JvmComponentDependencies>) {
action.execute(getDependencies())
}
/**
* Specify that this variant comes with a resource bundle.
*/
fun jar(action: Action<Unit>) {
sourceSet.resources.srcDirs.add(sourceSet.java.srcDirs.first().parentFile.resolve("resources"))
sourceSet.resources.includes.add("**/*.*")
tasks.named<Jar>(sourceSet.jarTaskName).configure {
include("**/*.*")
dependsOn(tasks.named<ProcessResources>(sourceSet.processResourcesTaskName))
manifest {
//this.attributes()
}
this.from(sourceSet.resources.srcDirs)
}
runtimeElements.outgoing.artifact(tasks.named(sourceSet.jarTaskName))
action.execute(Unit)
}
}
abstract class VariantExtension(
@Inject val objectFactory: ObjectFactory,
@Inject val project: Project
) {
fun platform(os: String, arch: String, f: VariantContainer.() -> Unit) {
val sourceSets = project.extensions.getByType(SourceSetContainer::class.java)
val sourceSetArch = arch.replace("-", "_")
val nameMain = "${os}${sourceSetArch.capitalize()}Main"
val platformMain = sourceSets.create(nameMain)
val tasks = project.tasks
tasks.register(platformMain.jarTaskName, Jar::class.java) {
archiveClassifier.set("$os-$arch")
}
val configurations = project.configurations
val objects = project.objects
val main = sourceSets.getByName("main")
val mainApi = configurations.getByName(main.apiElementsConfigurationName)
val mainRuntimeOnly = configurations.getByName(main.runtimeElementsConfigurationName)
mainApi.attributes {
val osAttribute = Attribute.of("org.gradle.native.operatingSystem", String::class.java)
attribute(osAttribute, "do_not_use_me")
}
val platformMainRuntimeElements = configurations.create(platformMain.runtimeElementsConfigurationName) {
extendsFrom(mainRuntimeOnly, mainApi)
isCanBeResolved = false
isCanBeConsumed = true
val osAttribute = Attribute.of("org.gradle.native.operatingSystem", String::class.java)
val archAttribute = Attribute.of("org.gradle.native.architecture", String::class.java)
val typeAttribute = Attribute.of("org.jetbrains.kotlin.platform.type", String::class.java)
val environmentAttribute = Attribute.of("org.gradle.jvm.environment", String::class.java)
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attribute(osAttribute, os)
attribute(archAttribute, arch)
attribute(typeAttribute, "jvm")
attribute(environmentAttribute, "standard-jvm")
}
outgoing.artifact(tasks.named(main.jarTaskName))
outgoing.artifact(tasks.named(platformMain.jarTaskName))
}
val javaComponent = project.components.getByName("java") as AdhocComponentWithVariants
javaComponent.addVariantsFromConfiguration(platformMainRuntimeElements) {
platformMain.runtimeClasspath.files.add(platformMain.resources.srcDirs.first())
}
val variantContainer = objectFactory.newInstance(
VariantContainer::class.java,
platformMainRuntimeElements,
platformMainRuntimeElements,
platformMain
)
variantContainer.f()
platformMainRuntimeElements.dependencies.addAll(variantContainer.getDependencies().runtimeOnly.dependencies.get())
/*
Setup dependencies for current platform. This will make in-module tests and demos work.
*/
val currentOperatingSystemName: String = DefaultNativePlatform.getCurrentOperatingSystem().toFamilyName()
val currentArchitectureName: String = arch()
if (currentOperatingSystemName == os && currentArchitectureName == arch) {
project.dependencies {
add("testRuntimeOnly", platformMain.output)
add("demoRuntimeOnly", platformMain.output)
for (i in platformMainRuntimeElements.dependencies) {
add("testRuntimeOnly", i)
add("demoRuntimeOnly", i)
}
}
}
}
}
class VariantPlugin : Plugin<Project> {
override fun apply(target: Project) {
val project = target
project.extensions.create("variants", VariantExtension::class.java)
}
}

View File

@@ -1,29 +0,0 @@
include("orx-convention", "orx-variant-plugin")
dependencyResolutionManagement {
repositories {
mavenCentral()
mavenLocal {
content {
includeGroup("org.openrndr")
}
}
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
// We use a regex to get the openrndr version from the primary catalog as there is no public Gradle API to parse catalogs.
val regEx = Regex("^openrndr[ ]*=[ ]*(?:\\{[ ]*require[ ]*=[ ]*)?\"(.*)\"[ ]*(?:\\})?", RegexOption.MULTILINE)
val openrndrVersion = regEx.find(File(rootDir,"../gradle/libs.versions.toml").readText())?.groupValues?.get(1) ?: error("can't find openrndr version")
create("sharedLibs") {
from("org.openrndr:openrndr-dependency-catalog:$openrndrVersion")
}
create("openrndr") {
from("org.openrndr:openrndr-module-catalog:$openrndrVersion")
}
}
}

View File

@@ -1,2 +0,0 @@
#/bin/bash
xvfb-run -e /dev/stdout java "$@"

View File

@@ -1,144 +1,6 @@
plugins {
alias(libs.plugins.nebula.release)
alias(libs.plugins.nmcp)
id("org.openrndr.extra.convention.dokka")
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.android.library) apply false
}
repositories {
mavenCentral()
}
tasks.register('buildMainReadme') {
doFirst {
def subProjects = project.subprojects
//.findAll { !it.name.contains("kinect-common") && !it.name.contains
// ("kinect-v1-") }
// Load README.md and find [begin, end] section to replace
def mainReadme = file("README.md")
def lines = mainReadme.readLines()
def begin = lines.findIndexOf { it == "<!-- __orxListBegin__ -->" }
def end = lines.findIndexOf { it == "<!-- __orxListEnd__ -->" }
if (begin == -1 || end == -1) {
println("Comments for orx list generation not found in README.md!")
return
}
def header = lines.subList(0, begin + 1)
def footer = lines.subList(end, lines.size())
def newReadme = []
for (line in header) {
newReadme.add(line)
}
// Search for the description at the top of the readme.
// Skip the hash character from the headline, then start
// on the next line and continue until the next empty line.
// Don't fall into Windows line breaks.
def descriptionRx = ~/(?s)#.*?\n(.+?)\n\r?\n/
// Note: the readme needs an empty line after the description
def orxMultiplatform = []
def orxJVMOnly = []
// Build orx list
for (sub in subProjects) {
def orxReadmeFile = sub.file("README.md")
if (orxReadmeFile.exists()) {
def orxReadmeText = orxReadmeFile.getText()
orxReadmeText.find(descriptionRx) {
description ->
def trimmedDescription = description[1].trim() //.strip() supports unicode, java11 only
.replace("\n", " ").replace("\r", "")
def path = sub.path.substring(1).replace(":", "/")
if (path.startsWith("orx-jvm")) {
orxJVMOnly.add("| [`${sub.name}`]($path/) " +
"| $trimmedDescription |")
} else {
orxMultiplatform.add("| [`${sub.name}`]($path/) " +
"| $trimmedDescription |")
}
}
} else {
println("${sub.name}/README.md not found!")
}
}
newReadme.add("\n## Multiplatform\n")
newReadme.add("| name" + "&nbsp;" * 36 + " | description |")
newReadme.add("| --- | --- |")
newReadme.addAll(orxMultiplatform)
newReadme.add("\n## JVM only\n")
newReadme.add("| name" + "&nbsp;" * 36 + " | description |")
newReadme.add("| --- | --- |")
newReadme.addAll(orxJVMOnly)
for (line in footer) {
newReadme.add(line)
}
// Write result
if (mainReadme.exists()) {
mainReadme.delete()
}
mainReadme.write(newReadme.join("\n"))
}
}
group = "org.openrndr.extra"
nmcpAggregation {
centralPortal {
username.set(findProperty("ossrhUsername") ?: System.getenv("OSSRH_USERNAME"))
password.set(findProperty("ossrhPassword") ?: System.getenv("OSSRH_PASSWORD"))
// publish manually from the portal
publishingType = "USER_MANAGED"
}
// Publish all projects that apply the 'maven-publish' plugin
publishAllProjectsProbablyBreakingProjectIsolation()
}
//nexusPublishing {
// repositories {
// sonatype {
// username.set(findProperty("ossrhUsername") ?: System.getenv("OSSRH_USERNAME"))
// password.set(findProperty("ossrhPassword") ?: System.getenv("OSSRH_PASSWORD"))
// nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/"))
// snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/"))
// }
// }
//}
subprojects {
// Equivalent Kotlin is: tasks.register<DependencyReportTask>("dependenciesAll") { ...
tasks.register("dependenciesAll", DependencyReportTask) {
group = HelpTasksPlugin.HELP_GROUP
description = "Displays all dependencies, including subprojects."
}
}
dependencies {
subprojects.findAll {
it.name.startsWith("orx-") && !it.name.contains("-catalog")
}.each { subproject ->
dokka(project(subproject.path))
}
}
class SleepTask extends DefaultTask {
@TaskAction
void action() {
sleep(60 * 5 * 1000)
}
}
tasks.register("sleep", SleepTask)
gradle.buildFinished {
println("\n")
println("orx = \"${version}\"")
}

View File

@@ -1 +0,0 @@
**exported*

Binary file not shown.

Binary file not shown.

View File

@@ -1,219 +0,0 @@
{
"asset": {
"generator": "COLLADA2GLTF",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"nodes": [
{
"children": [
2,
1
],
"matrix": [
0.009999999776482582,
0.0,
0.0,
0.0,
0.0,
0.009999999776482582,
0.0,
0.0,
0.0,
0.0,
0.009999999776482582,
0.0,
0.0,
0.0,
0.0,
1.0
]
},
{
"matrix": [
-0.7289686799049377,
0.0,
-0.6845470666885376,
0.0,
-0.4252049028873444,
0.7836934328079224,
0.4527972936630249,
0.0,
0.5364750623703003,
0.6211478114128113,
-0.571287989616394,
0.0,
400.1130065917969,
463.2640075683594,
-431.0780334472656,
1.0
],
"camera": 0
},
{
"mesh": 0
}
],
"cameras": [
{
"perspective": {
"aspectRatio": 1.5,
"yfov": 0.6605925559997559,
"zfar": 10000.0,
"znear": 1.0
},
"type": "perspective"
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 2,
"TEXCOORD_0": 3
},
"indices": 0,
"mode": 4,
"material": 0
}
],
"name": "LOD3spShape"
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5123,
"count": 12636,
"max": [
2398
],
"min": [
0
],
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 2399,
"max": [
0.9995989799499512,
0.999580979347229,
0.9984359741210938
],
"min": [
-0.9990839958190918,
-1.0,
-0.9998319745063782
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 28788,
"componentType": 5126,
"count": 2399,
"max": [
96.17990112304688,
163.97000122070313,
53.92519760131836
],
"min": [
-69.29850006103516,
9.929369926452637,
-61.32819747924805
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 0,
"componentType": 5126,
"count": 2399,
"max": [
0.9833459854125976,
0.9800369739532472
],
"min": [
0.026409000158309938,
0.01996302604675293
],
"type": "VEC2"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0
},
"emissiveFactor": [
0.0,
0.0,
0.0
],
"name": "blinn3-fx"
}
],
"textures": [
{
"sampler": 0,
"source": 0
}
],
"images": [
{
"uri": "DuckCM.png"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9986,
"wrapS": 10497,
"wrapT": 10497
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 76768,
"byteLength": 25272,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 57576,
"byteStride": 12,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 57576,
"byteLength": 19192,
"byteStride": 8,
"target": 34962
}
],
"buffers": [
{
"byteLength": 102040,
"uri": "Duck0.bin"
}
]
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

View File

@@ -1,193 +0,0 @@
{
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 11808,
"max" : [
11807
],
"min" : [
0
],
"type" : "SCALAR"
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 11808,
"max" : [
1.336914,
0.950195,
0.825684
],
"min" : [
-1.336914,
-0.974609,
-0.800781
],
"type" : "VEC3"
},
{
"bufferView" : 2,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 11808,
"max" : [
0.996339,
0.999958,
0.999929
],
"min" : [
-0.996339,
-0.985940,
-0.999994
],
"type" : "VEC3"
},
{
"bufferView" : 3,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 11808,
"max" : [
0.998570,
0.999996,
0.999487,
1.000000
],
"min" : [
-0.999233,
-0.999453,
-0.999812,
1.000000
],
"type" : "VEC4"
},
{
"bufferView" : 4,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 11808,
"max" : [
0.999884,
0.884359
],
"min" : [
0.000116,
0.000116
],
"type" : "VEC2"
}
],
"asset" : {
"generator" : "VKTS glTF 2.0 exporter",
"version" : "2.0"
},
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 23616,
"byteOffset" : 0,
"target" : 34963
},
{
"buffer" : 0,
"byteLength" : 141696,
"byteOffset" : 23616,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 141696,
"byteOffset" : 165312,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 188928,
"byteOffset" : 307008,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 94464,
"byteOffset" : 495936,
"target" : 34962
}
],
"buffers" : [
{
"byteLength" : 590400,
"uri" : "Suzanne.bin"
}
],
"images" : [
{
"uri" : "Suzanne_BaseColor.png"
},
{
"uri" : "Suzanne_MetallicRoughness.png"
}
],
"materials" : [
{
"name" : "Suzanne",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicRoughnessTexture" : {
"index" : 1
}
}
}
],
"meshes" : [
{
"name" : "Suzanne",
"primitives" : [
{
"attributes" : {
"NORMAL" : 2,
"POSITION" : 1,
"TANGENT" : 3,
"TEXCOORD_0" : 4
},
"indices" : 0,
"material" : 0,
"mode" : 4
}
]
}
],
"nodes" : [
{
"mesh" : 0,
"name" : "Suzanne"
}
],
"samplers" : [
{}
],
"scene" : 0,
"scenes" : [
{
"nodes" : [
0
]
}
],
"textures" : [
{
"sampler" : 0,
"source" : 0
},
{
"sampler" : 0,
"source" : 1
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,2 +0,0 @@
# Blender 4.1.1 MTL File: 'None'
# www.blender.org

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
Sound author
http://jazzy.junggle.net/
License
https://creativecommons.org/licenses/by/4.0/
Downloaded from
https://freesound.org/s/26777/

View File

@@ -1,14 +0,0 @@
smiling
dumb
happy
red
green
blue
pink
white
black
purple
crying
running
jumping
eating

View File

@@ -1,14 +0,0 @@
man
woman
child
boy
girl
house
flower
chair
table
car
ground
garden
airplane
school

View File

@@ -1,14 +0,0 @@
in
under
at
on
near
amongst
in front of
behind
below
next to
on top of
opposite
with
close to

View File

@@ -1,14 +0,0 @@
car
fish
flower
eagle
garbage
computer
beautiful
ugly
random
man
woman
child
missing
magic

View File

@@ -1,37 +0,0 @@
plugins {
id("org.openrndr.extra.convention.kotlin-multiplatform")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(openrndr.math)
api(openrndr.shape)
implementation(project(":orx-noise"))
}
}
val commonTest by getting {
dependencies {
implementation(project(":orx-shapes"))
implementation(openrndr.shape)
}
}
val jvmDemo by getting {
dependencies {
implementation(project(":orx-triangulation"))
implementation(project(":orx-shapes"))
implementation(project(":orx-noise"))
implementation(openrndr.shape)
implementation(project(":math"))
implementation(project(":orx-camera"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation(project(":orx-marching-squares"))
implementation(project(":orx-text-writer"))
implementation(project(":orx-obj-loader"))
implementation(project(":orx-palette"))
}
}
}
}

View File

@@ -1,35 +0,0 @@
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)
}
}
}
}

View File

@@ -1,654 +0,0 @@
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.Vector3D
import com.icegps.math.geometry.degrees
import org.openrndr.KEY_ARROW_DOWN
import org.openrndr.KEY_ARROW_UP
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.TextSettingMode
import org.openrndr.draw.loadFont
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.extra.noise.gradientPerturbFractal
import org.openrndr.extra.textwriter.writer
import org.openrndr.extra.triangulation.DelaunayTriangulation
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Segment2D
import org.openrndr.shape.Segment3D
import org.openrndr.shape.ShapeContour
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
fun main() = application {
configure {
width = 720
height = 480
title = "Delaunator"
}
program {
val points3D = (0 until height).step(36).map { y ->
(0 until width).step(36).map { x ->
gradientPerturbFractal(
300,
frequency = 0.8,
position = Vector3(x.toDouble(), y.toDouble(), seconds)
)
}
}.flatten().map {
it.copy(z = it.z * 100)
}
/*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
width = width,
height = height,
volcanoCount = 3
)*/
// val points3D = coordinateGenerate(width, height)
val zs = points3D.map { it.z }
println("zs = ${zs}")
val associate: MutableMap<Vector2, Double> = points3D.associate {
Vector2(it.x, it.y) to it.z
}.toMutableMap()
val delaunay = DelaunayTriangulation(associate.map { it.key })
//println(points3D.niceStr())
extend(Camera2D())
println("draw")
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 useInterpolation = false
var sampleLinear = false
keyboard.keyDown.listen {
logEnabled = true
println(it)
when (it.key) {
KEY_ARROW_UP -> targetHeight++
KEY_ARROW_DOWN -> targetHeight--
73 -> useInterpolation = !useInterpolation
83 -> sampleLinear = !sampleLinear
}
}
extend {
val triangles = delaunay.triangles()
val segments = mutableListOf<Segment2D>()
drawer.clear(ColorRGBa.BLACK)
val indexDiff = (frameCount / 1000) % triangles.size
for ((i, triangle) in triangles.withIndex()) {
val segment2DS = triangle.contour.segments.filter {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
if (startZ < endZ) {
targetHeight in startZ..endZ
} else {
targetHeight in endZ..startZ
}
}
if (segment2DS.size == 2) {
val vector2s = segment2DS.map {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
val start = Vector3(it.start.x, it.start.y, startZ)
val end = Vector3(it.end.x, it.end.y, endZ)
if (startZ < endZ) {
start to end
} else {
end to start
}
}.map { (start, end) ->
val segment3D = Segment3D(start, end)
val vector3 =
segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
vector3
}.map {
associate[it.xy] = it.z
it.xy
}
val element = Segment2D(vector2s[0], vector2s[1])
segments.add(element)
}
drawer.fill = if (indexDiff == i) {
ColorRGBa.CYAN
} else {
ColorRGBa.PINK.shade(1.0 - i / (triangles.size * 1.2))
}
drawer.stroke = ColorRGBa.PINK.shade(i / (triangles.size * 1.0) + 0.1)
drawer.contour(triangle.contour)
}
val sorted = connectAllSegments(segments)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 2.0
if (logEnabled) {
segments.forEach {
println("${it.start} -> ${it.end}")
}
println("=====")
}
sorted.forEach {
it.forEach {
if (logEnabled) println("${it.start} -> ${it.end}")
drawer.lineSegment(it.start, it.end)
drawer.fill = ColorRGBa.WHITE
}
if (logEnabled) println("=")
drawer.fill = ColorRGBa.YELLOW
if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
}
/*for (y in 0 until (area.height / 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))
}
}*/
val contours = 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 - targetHeight
},
area = drawer.bounds,
cellSize = 4.0,
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")
drawer.stroke = null
if (true) contours.forEach {
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
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)
writer {
drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
text(targetHeight.toString())
}
logEnabled = false
}
}
}
/**
* 射线法判断点是否在单个三角形内
*/
fun isPointInTriangle(point: Vector2, triangle: List<Vector2>): 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
}
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 triangle 三角形的三个顶点
* @return 三维点 (x, y, z)
*/
fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
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 Vector3(point.x, point.y, z)
}
/**
* 计算点在三角形中的重心坐标
*/
fun calculateBarycentricCoordinates(
point: Vector2,
v1: Vector3,
v2: Vector3,
v3: Vector3
): 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)
}
fun connectAllSegments(segments: List<Segment2D>): List<List<Segment2D>> {
val remaining = segments.toMutableList()
val allPaths = mutableListOf<List<Segment2D>>()
while (remaining.isNotEmpty()) {
val path = mutableListOf<Segment2D>()
// 开始新路径
path.add(remaining.removeAt(0))
var changed: Boolean
do {
changed = false
// 向前扩展
val lastEnd = path.last().end
val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
if (forwardSegment != null) {
val connectedSegment = if (forwardSegment.start == lastEnd) {
forwardSegment // 正向
} else {
Segment2D(forwardSegment.end, forwardSegment.start) // 反向
}
path.add(connectedSegment)
remaining.remove(forwardSegment)
changed = true
}
// 向后扩展
val firstStart = path.first().start
val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
if (backwardSegment != null) {
val connectedSegment = if (backwardSegment.end == firstStart) {
backwardSegment // 正向
} else {
Segment2D(backwardSegment.end, backwardSegment.start) // 反向
}
path.add(0, connectedSegment)
remaining.remove(backwardSegment)
changed = true
}
} while (changed && remaining.isNotEmpty())
allPaths.add(path)
}
return allPaths
}
fun connectSegmentsEfficient(segments: List<Segment2D>): List<Segment2D> {
if (segments.isEmpty()) return emptyList()
val remaining = segments.toMutableList()
val connected = mutableListOf<Segment2D>()
// 构建端点查找表
val startMap = mutableMapOf<Vector2, MutableList<Segment2D>>()
val endMap = mutableMapOf<Vector2, MutableList<Segment2D>>()
segments.forEach { segment ->
startMap.getOrPut(segment.start) { mutableListOf() }.add(segment)
endMap.getOrPut(segment.end) { mutableListOf() }.add(segment)
}
// 从第一个线段开始
var currentSegment = remaining.removeAt(0)
connected.add(currentSegment)
// 更新查找表
startMap[currentSegment.start]?.remove(currentSegment)
endMap[currentSegment.end]?.remove(currentSegment)
// 向前连接
while (true) {
val nextFromStart = startMap[currentSegment.end]?.firstOrNull()
val nextFromEnd = endMap[currentSegment.end]?.firstOrNull()
when {
nextFromStart != null -> {
// 正向连接
connected.add(nextFromStart)
remaining.remove(nextFromStart)
startMap[nextFromStart.start]?.remove(nextFromStart)
endMap[nextFromStart.end]?.remove(nextFromStart)
currentSegment = nextFromStart
}
nextFromEnd != null -> {
// 反向连接
val reversed = Segment2D(nextFromEnd.end, nextFromEnd.start)
connected.add(reversed)
remaining.remove(nextFromEnd)
startMap[nextFromEnd.start]?.remove(nextFromEnd)
endMap[nextFromEnd.end]?.remove(nextFromEnd)
currentSegment = reversed
}
else -> break
}
}
// 向后连接
currentSegment = connected.first()
while (true) {
val prevFromEnd = endMap[currentSegment.start]?.firstOrNull()
val prevFromStart = startMap[currentSegment.start]?.firstOrNull()
when {
prevFromEnd != null -> {
// 正向连接到开头
connected.add(0, prevFromEnd)
remaining.remove(prevFromEnd)
startMap[prevFromEnd.start]?.remove(prevFromEnd)
endMap[prevFromEnd.end]?.remove(prevFromEnd)
currentSegment = prevFromEnd
}
prevFromStart != null -> {
// 反向连接到开头
val reversed = Segment2D(prevFromStart.end, prevFromStart.start)
connected.add(0, reversed)
remaining.remove(prevFromStart)
startMap[prevFromStart.start]?.remove(prevFromStart)
endMap[prevFromStart.end]?.remove(prevFromStart)
currentSegment = reversed
}
else -> break
}
}
return connected
}
fun connectSegments(segments: List<Segment2D>): List<Segment2D> {
if (segments.isEmpty()) return emptyList()
val remaining = segments.toMutableList()
val connected = mutableListOf<Segment2D>()
// 从第一个线段开始,保持原方向
connected.add(remaining.removeAt(0))
while (remaining.isNotEmpty()) {
val lastEnd = connected.last().end
var found = false
// 查找可以连接的线段
for (i in remaining.indices) {
val segment = remaining[i]
// 检查四种可能的连接方式
when {
// 正向连接:当前终点 == 线段起点
segment.start == lastEnd -> {
connected.add(segment)
remaining.removeAt(i)
found = true
break
}
// 反向连接:当前终点 == 线段终点,需要反转线段
segment.end == lastEnd -> {
connected.add(Segment2D(segment.end, segment.start)) // 反转
remaining.removeAt(i)
found = true
break
}
// 正向连接另一端:当前起点 == 线段终点,需要插入到前面
segment.end == connected.first().start -> {
connected.add(0, Segment2D(segment.end, segment.start)) // 反转后插入开头
remaining.removeAt(i)
found = true
break
}
// 反向连接另一端:当前起点 == 线段起点,需要反转并插入到前面
segment.start == connected.first().start -> {
connected.add(0, segment) // 直接插入开头(已经是正确方向)
remaining.removeAt(i)
found = true
break
}
}
}
if (!found) break // 无法找到连接线段
}
return connected
}
fun calculatePositionRatio(value: Double, rangeStart: Double, rangeEnd: Double): Double {
if (rangeStart == rangeEnd) return 0.0 // 避免除零
val ratio = (value - rangeStart) / (rangeEnd - rangeStart)
return ratio.coerceIn(0.0, 1.0)
}
fun sortLinesEfficient(lines: List<Segment2D>): List<Segment2D> {
if (lines.isEmpty()) return emptyList()
// 创建起点到线段的映射
val startMap = lines.associateBy { it.start }
val sorted = mutableListOf<Segment2D>()
// 找到起点(没有其他线段的终点指向它的起点)
var currentLine = lines.firstOrNull { line ->
lines.none { it.end == line.start }
} ?: lines.first()
sorted.add(currentLine)
while (true) {
val nextLine = startMap[currentLine.end]
if (nextLine == null || nextLine == lines.first()) break
sorted.add(nextLine)
currentLine = nextLine
}
return sorted
}
fun sortLines(lines: List<Segment2D>): List<Segment2D> {
if (lines.isEmpty()) return emptyList()
val remaining = lines.toMutableList()
val sorted = mutableListOf<Segment2D>()
// 从第一个线段开始
sorted.add(remaining.removeAt(0))
while (remaining.isNotEmpty()) {
val lastEnd = sorted.last().end
var found = false
// 查找下一个线段
for (i in remaining.indices) {
if (remaining[i].start == lastEnd) {
sorted.add(remaining.removeAt(i))
found = true
break
}
}
if (!found) break // 无法找到下一个线段
}
return sorted
}
fun findLineLoops(lines: List<Segment2D>): List<List<Segment2D>> {
val remaining = lines.toMutableList()
val loops = mutableListOf<List<Segment2D>>()
while (remaining.isNotEmpty()) {
val loop = findSingleLoop(remaining)
if (loop.isNotEmpty()) {
loops.add(loop)
// 移除已使用的线段
loop.forEach { line ->
remaining.remove(line)
}
} else {
// 无法形成环的线段
break
}
}
return loops
}
fun findSingleLoop(remaining: MutableList<Segment2D>): List<Segment2D> {
if (remaining.isEmpty()) return emptyList()
val loop = mutableListOf<Segment2D>()
loop.add(remaining.removeAt(0))
// 向前查找连接
while (remaining.isNotEmpty()) {
val lastEnd = loop.last().end
val nextIndex = remaining.indexOfFirst { it.start == lastEnd }
if (nextIndex == -1) {
// 尝试向后查找连接
val firstStart = loop.first().start
val prevIndex = remaining.indexOfFirst { it.end == firstStart }
if (prevIndex != -1) {
loop.add(0, remaining.removeAt(prevIndex))
} else {
break // 无法继续连接
}
} else {
loop.add(remaining.removeAt(nextIndex))
}
// 检查是否形成闭环
if (loop.last().end == loop.first().start) {
return loop
}
}
// 如果没有形成闭环,返回空列表(或者可以根据需求返回部分环)
remaining.addAll(loop) // 将线段放回剩余列表
return emptyList()
}
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
)
}
fun coordinateGenerate(width: Int, height: Int): List<Vector3D> {
val minX = 0.0
val maxX = width.toDouble()
val minY = 0.0
val maxY = height.toDouble()
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<Vector3D> {
val center = Vector3D(0.0, 0.0, 0.0)
val direction = Vector3D(0.0, 1.0, -1.0)
return (0..360).step(36).map<Int, List<Vector3D>> { degrees: Int ->
val newDirection = direction.rotateAroundZ(angle = degrees.degrees)
(0..5).map {
center + newDirection * it * 100
}
}.flatten()
}
fun Vector3D.niceStr(): String {
return "[$x, $y, $z]".format(this)
}
fun List<Vector3D>.niceStr(): String {
return joinToString(", ", "[", "]") {
it.niceStr()
}
}

View File

@@ -1,267 +0,0 @@
import org.openrndr.KEY_ARROW_DOWN
import org.openrndr.KEY_ARROW_UP
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.TextSettingMode
import org.openrndr.draw.loadFont
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.extra.textwriter.writer
import org.openrndr.extra.triangulation.DelaunayTriangulation
import org.openrndr.extra.triangulation.DelaunayTriangulation3D
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Path3D
import org.openrndr.shape.Segment3D
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
fun main() = application {
configure {
width = 720
height = 720
title = "Delaunator"
multisample = WindowMultisample.SampleCount(8)
}
program {
/*val points3D = (0 until height).step(36).map { y ->
(0 until width).step(36).map { x ->
gradientPerturbFractal(
300,
frequency = 0.8,
position = Vector3(x.toDouble(), y.toDouble(), seconds)
)
}
}.flatten().map {
it.copy(x = it.x - width / 2, y = it.y - height / 2, z = it.z * 100)
}*/
/*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
width = width,
height = height,
volcanoCount = 3
)*/
val points3D = coordinateGenerate(width, height).map {
it.copy(x = it.x - width / 2, y = it.y - height / 2, z = it.z * 10)
}
val zs = points3D.map { it.z }
println("zs = ${zs}")
val associate: MutableMap<Vector2, Double> = points3D.associate {
Vector2(it.x, it.y) to it.z
}.toMutableMap()
val delaunay = DelaunayTriangulation(associate.map { it.key })
val delaunay3D = DelaunayTriangulation3D(points3D.map { Vector3(it.x, it.y, it.z) })
//println(points3D.niceStr())
//extend(Camera2D())
val cam = Orbital()
extend(cam) {
eye = Vector3(x = 100.0, y = 100.0, z = 0.0)
lookAt = Vector3(x = 1.6, y = -1.9, z = 1.2)
}
println("draw")
var targetHeight: Double = zs.average()
var logEnabled = true
var useInterpolation = false
var sampleLinear = false
keyboard.keyDown.listen {
logEnabled = true
println(it)
when (it.key) {
KEY_ARROW_UP -> targetHeight++
KEY_ARROW_DOWN -> targetHeight--
73 -> useInterpolation = !useInterpolation
83 -> sampleLinear = !sampleLinear
}
}
val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj")
extend {
val triangles = delaunay3D.triangles()
val segments = mutableListOf<Segment3D>()
drawer.clear(ColorRGBa.BLACK)
val indexDiff = (frameCount / 1000) % triangles.size
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb = normalize(v_viewNormal) * 0.5 + vec3(0.5);
""".trimIndent()
}
drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
// 绘制等高线段区域
for ((i, triangle) in triangles.withIndex()) {
val segment2DS = triangle.segments.filter {
val startZ = it.start.z
val endZ = it.end.z
if (startZ < endZ) {
targetHeight in startZ..endZ
} else {
targetHeight in endZ..startZ
}
}
if (segment2DS.size == 2) {
val vector2s = segment2DS.map {
val startZ = it.start.z
val endZ = it.end.z
val start = Vector3(it.start.x, it.start.y, startZ)
val end = Vector3(it.end.x, it.end.y, endZ)
if (startZ < endZ) {
start to end
} else {
end to start
}
}.map { (start, end) ->
val segment3D = Segment3D(start, end)
val vector3 =
segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
vector3
}.onEach {
associate[it.xy] = it.z
}
val element = Segment3D(vector2s[0], vector2s[1])
segments.add(element)
}
drawer.strokeWeight = 20.0
drawer.stroke = ColorRGBa.PINK
//drawer.contour(triangle.contour)
drawer.path(triangle.path)
}
val sorted = connectAllSegments(segments)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 2.0
if (logEnabled) {
segments.forEach {
println("${it.start} -> ${it.end}")
}
println("=====")
}
sorted.forEach {
it.forEach {
if (logEnabled) println("${it.start} -> ${it.end}")
drawer.lineSegment(it.start, it.end)
drawer.fill = ColorRGBa.WHITE
}
if (logEnabled) println("=")
drawer.fill = ColorRGBa.YELLOW
// if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
}
// 结束绘制等高线
/*for (y in 0 until (area.height / 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))
}
}*/
val contours = findContours(
f = {
val triangle = triangles.firstOrNull { triangle ->
isPointInTriangle3D(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,
)
)
interpolate.z - targetHeight
},
area = drawer.bounds.movedTo(Vector2(-width / 2.0, -height / 2.0)),
cellSize = 4.0,
useInterpolation = useInterpolation
)
if (logEnabled) println("useInterpolation = $useInterpolation")
drawer.stroke = null
contours.map {
if (false) drawer.contour(it)
it.segments.map {
Segment3D(
it.start.vector3(),
it.end.vector3()
)
}
}.forEach {
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
drawer.path(Path3D.fromSegments(it, closed = true))
}
if (false) writer {
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
text(targetHeight.toString())
}
logEnabled = false
}
}
}
data class Triangle3D(
val x1: Vector3,
val x2: Vector3,
val x3: Vector3,
) {
fun toList(): List<Vector3> = listOf(x1, x2, x3)
}
fun connectAllSegments(segments: List<Segment3D>): List<List<Segment3D>> {
val remaining = segments.toMutableList()
val allPaths = mutableListOf<List<Segment3D>>()
while (remaining.isNotEmpty()) {
val path = mutableListOf<Segment3D>()
// 开始新路径
path.add(remaining.removeAt(0))
var changed: Boolean
do {
changed = false
// 向前扩展
val lastEnd = path.last().end
val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
if (forwardSegment != null) {
val connectedSegment = if (forwardSegment.start == lastEnd) {
forwardSegment // 正向
} else {
Segment3D(forwardSegment.end, forwardSegment.start) // 反向
}
path.add(connectedSegment)
remaining.remove(forwardSegment)
changed = true
}
// 向后扩展
val firstStart = path.first().start
val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
if (backwardSegment != null) {
val connectedSegment = if (backwardSegment.end == firstStart) {
backwardSegment // 正向
} else {
Segment3D(backwardSegment.end, backwardSegment.start) // 反向
}
path.add(0, connectedSegment)
remaining.remove(backwardSegment)
changed = true
}
} while (changed && remaining.isNotEmpty())
allPaths.add(path)
}
return allPaths
}

View File

@@ -1,94 +0,0 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* A simple demonstration of using the `findContours` method provided by `orx-marching-squares`.
*
* `findContours` lets one generate contours by providing a mathematical function to be
* sampled within the provided area and with the given cell size. Contours are generated
* between the areas in which the function returns positive and negative values.
*
* In this example, the `f` function returns the distance of a point to the center of the window minus 200.0.
* Therefore, sampled locations which are less than 200 pixels away from the center return
* negative values and all others return positive values, effectively generating a circle of radius 200.0.
*
* Try increasing the cell size to see how the precision of the circle reduces.
*
* The circular contour created in this program has over 90 segments. The number of segments depends on the cell
* size, and the resulting radius.
*/
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend(Camera2D())
var showLog = true
val target = Vector2(0.0, 0.0)
val points3D = (0..10).map { x ->
(0..10).map { y ->
Vector3(x.toDouble(), y.toDouble(), x * y * 1.0)
}
}
extend {
drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.PINK
fun f3(v: Vector2): Double {
val distance = drawer.bounds.center.distanceTo(v)
return when (distance) {
in 0.0..<100.0 -> -3.0
in 100.0..<200.0 -> 1.0
in 200.0..300.0 -> -1.0
else -> distance
}
}
fun f(v: Vector2): Double {
val distanceTo = v.distanceTo(target)
return (distanceTo - 100.0).also {
if (showLog) println(
buildString {
appendLine("${v} distanceTo ${target} = ${distanceTo}")
appendLine("distanceTo - 100.0 = ${distanceTo - 100.0}")
}
)
}
}
val points = mutableListOf<Vector2>()
fun f1(v: Vector2): Double {
val result = if (v.x == v.y * 2 || v.x * 2 == v.y) {
points.add(v)
-1.0
} else 0.0
return result.also {
if (showLog) {
println("$v -> $result")
}
}
}
val contours = findContours(::f3, drawer.bounds, 4.0)
drawer.fill = null
drawer.contours(contours)
if (showLog) {
println(
buildString {
for ((index, contour) in contours.withIndex()) {
appendLine("index = ${index}, $contour")
}
}
)
}
showLog = false
}
}
}

View File

@@ -1,373 +0,0 @@
import com.icegps.math.geometry.Vector3D
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
object HeightmapVolcanoGenerator {
// 基础火山高度图
fun generateVolcanoHeightmap(
width: Int = 100,
height: Int = 100,
centerX: Double = 50.0,
centerY: Double = 50.0,
maxHeight: Double = 60.0,
craterRadius: Double = 8.0,
volcanoRadius: Double = 30.0
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
for (x in 0 until width) {
for (y in 0 until height) {
// 计算到火山中心的距离
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
// 计算基础火山高度
var z = calculateVolcanoHeight(distance, craterRadius, volcanoRadius, maxHeight)
// 添加噪声细节
val noise = perlinNoise(x * 0.1, y * 0.1, 0.1) * 3.0
z = max(0.0, z + noise)
points.add(Vector3D(x.toDouble(), y.toDouble(), z))
}
}
return points
}
// 复合火山群高度图
fun generateVolcanoClusterHeightmap(
width: Int = 150,
height: Int = 150,
volcanoCount: Int = 3
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val volcanoes = generateRandomVolcanoPositions(volcanoCount, width, height)
for (x in (0 until width).step(25)) {
for (y in (0 until height).step(25)) {
var totalZ = 0.0
// 叠加所有火山的影响
for (volcano in volcanoes) {
val dx = x - volcano.x
val dy = y - volcano.y
val distance = sqrt(dx * dx + dy * dy)
if (distance <= volcano.radius) {
val volcanoHeight = calculateVolcanoHeight(
distance,
volcano.craterRadius,
volcano.radius,
volcano.maxHeight
)
totalZ += volcanoHeight
}
}
// 基础地形
val baseNoise = perlinNoise(x * 0.02, y * 0.02, 0.05) * 5.0
val detailNoise = perlinNoise(x * 0.1, y * 0.1, 0.2) * 2.0
points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ + baseNoise + detailNoise))
}
}
return points
}
// 带熔岩流的火山高度图
fun generateVolcanoWithLavaHeightmap(
width: Int = 100,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val centerX = width / 2.0
val centerY = height / 2.0
// 生成熔岩流路径
val lavaFlows = generateLavaFlowPaths(centerX, centerY, 3)
for (x in 0 until width) {
for (y in 0 until height) {
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
// 基础火山高度
var z = calculateVolcanoHeight(distance, 10.0, 35.0, 70.0)
// 添加熔岩流
z += calculateLavaFlowEffect(x.toDouble(), y.toDouble(), lavaFlows)
// 侵蚀效果
z += calculateErosionEffect(x.toDouble(), y.toDouble(), distance, z)
points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
}
}
return points
}
// 破火山口高度图
fun generateCalderaHeightmap(
width: Int = 100,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val centerX = width / 2.0
val centerY = height / 2.0
for (x in 0 until width) {
for (y in 0 until height) {
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
var z = calculateCalderaHeight(distance, 15.0, 45.0, 50.0)
// 内部平坦区域细节
if (distance < 20) {
z += perlinNoise(x * 0.2, y * 0.2, 0.3) * 1.5
}
points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
}
}
return points
}
// 线性火山链高度图
fun generateVolcanoChainHeightmap(
width: Int = 200,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
// 在一条线上生成多个火山
val chainCenters = listOf(
Vector3D(30.0, 50.0, 0.0),
Vector3D(70.0, 50.0, 0.0),
Vector3D(110.0, 50.0, 0.0),
Vector3D(150.0, 50.0, 0.0),
Vector3D(170.0, 50.0, 0.0)
)
for (x in 0 until width) {
for (y in 0 until height) {
var totalZ = 0.0
for (center in chainCenters) {
val dx = x - center.x
val dy = y - center.y
val distance = sqrt(dx * dx + dy * dy)
if (distance <= 25.0) {
val volcanoZ = calculateVolcanoHeight(distance, 6.0, 25.0, 40.0)
totalZ += volcanoZ
}
}
// 添加基底地形,模拟山脉链
val baseRidge = calculateMountainRidge(x.toDouble(), y.toDouble(), width, height)
totalZ += baseRidge
points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ))
}
}
return points
}
// 辅助函数
private data class VolcanoInfo(
val x: Double,
val y: Double,
val radius: Double,
val craterRadius: Double,
val maxHeight: Double
)
private data class LavaFlowInfo(
val startX: Double,
val startY: Double,
val angle: Double, // 弧度
val length: Double,
val width: Double,
val intensity: Double
)
private fun calculateVolcanoHeight(
distance: Double,
craterRadius: Double,
volcanoRadius: Double,
maxHeight: Double
): Double {
return when {
distance <= craterRadius -> {
// 火山口 - 中心凹陷
val craterDepth = maxHeight * 0.4
craterDepth * (1.0 - distance / craterRadius)
}
distance <= volcanoRadius -> {
// 火山锥
val slopeDistance = distance - craterRadius
val maxSlopeDistance = volcanoRadius - craterRadius
val normalized = slopeDistance / maxSlopeDistance
maxHeight * (1.0 - normalized * normalized)
}
else -> 0.0
}
}
private fun calculateCalderaHeight(
distance: Double,
innerRadius: Double,
outerRadius: Double,
rimHeight: Double
): Double {
return when {
distance <= innerRadius -> {
// 平坦的破火山口底部
rimHeight * 0.2
}
distance <= outerRadius -> {
// 陡峭的边缘
val rimDistance = distance - innerRadius
val rimWidth = outerRadius - innerRadius
val normalized = rimDistance / rimWidth
rimHeight * (1.0 - (1.0 - normalized) * (1.0 - normalized))
}
else -> {
// 外部平缓斜坡
val externalDistance = distance - outerRadius
rimHeight * exp(-externalDistance * 0.08)
}
}
}
private fun calculateLavaFlowEffect(x: Double, y: Double, lavaFlows: List<LavaFlowInfo>): Double {
var effect = 0.0
for (flow in lavaFlows) {
val dx = x - flow.startX
val dy = y - flow.startY
// 计算到熔岩流中心线的距离
val flowDirX = cos(flow.angle)
val flowDirY = sin(flow.angle)
val projection = dx * flowDirX + dy * flowDirY
if (projection in 0.0..flow.length) {
val perpendicularX = dx - projection * flowDirX
val perpendicularY = dy - projection * flowDirY
val perpendicularDist = sqrt(perpendicularX * perpendicularX + perpendicularY * perpendicularY)
if (perpendicularDist <= flow.width) {
val widthFactor = 1.0 - (perpendicularDist / flow.width)
val lengthFactor = 1.0 - (projection / flow.length)
effect += flow.intensity * widthFactor * lengthFactor
}
}
}
return effect
}
private fun calculateErosionEffect(x: Double, y: Double, distance: Double, height: Double): Double {
// 基于坡度的侵蚀
val slopeNoise = perlinNoise(x * 0.15, y * 0.15, 0.1) * 2.0
// 基于距离的侵蚀
val distanceErosion = if (distance > 25) perlinNoise(x * 0.08, y * 0.08, 0.05) * 1.5 else 0.0
return slopeNoise + distanceErosion
}
private fun calculateMountainRidge(x: Double, y: Double, width: Int, height: Int): Double {
// 创建山脉基底
val ridgeCenter = height / 2.0
val distanceToRidge = abs(y - ridgeCenter)
val ridgeWidth = height * 0.3
if (distanceToRidge <= ridgeWidth) {
val ridgeFactor = 1.0 - (distanceToRidge / ridgeWidth)
return ridgeFactor * 15.0 * perlinNoise(x * 0.01, y * 0.01, 0.02)
}
return 0.0
}
private fun generateRandomVolcanoPositions(count: Int, width: Int, height: Int): List<VolcanoInfo> {
return List(count) {
VolcanoInfo(
x = (width * 0.2 + random() * width * 0.6),
y = (height * 0.2 + random() * height * 0.6),
radius = 20.0 + random() * 20.0,
craterRadius = 5.0 + random() * 7.0,
maxHeight = 25.0 + random() * 35.0
)
}
}
private fun generateLavaFlowPaths(centerX: Double, centerY: Double, count: Int): List<LavaFlowInfo> {
return List(count) {
LavaFlowInfo(
startX = centerX,
startY = centerY,
angle = random() * 2 * PI,
length = 20.0 + random() * 15.0,
width = 2.0 + random() * 3.0,
intensity = 5.0 + random() * 8.0
)
}
}
private fun perlinNoise(x: Double, y: Double, frequency: Double): Double {
// 简化的柏林噪声实现
val x0 = floor(x * frequency)
val y0 = floor(y * frequency)
val x1 = x0 + 1
val y1 = y0 + 1
fun grad(ix: Int, iy: Int): Double {
val random = sin(ix * 12.9898 + iy * 78.233) * 43758.5453
return (random % 1.0) * 2 - 1
}
fun interpolate(a: Double, b: Double, w: Double): Double {
return a + (b - a) * (w * w * (3 - 2 * w))
}
val g00 = grad(x0.toInt(), y0.toInt())
val g10 = grad(x1.toInt(), y0.toInt())
val g01 = grad(x0.toInt(), y1.toInt())
val g11 = grad(x1.toInt(), y1.toInt())
val tx = x * frequency - x0
val ty = y * frequency - y0
val n0 = interpolate(g00, g10, tx)
val n1 = interpolate(g01, g11, tx)
return interpolate(n0, n1, ty)
}
private fun random(): Double = Math.random()
}

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48" height="48" version="1.2" viewBox="0 0 12.7 12.7" xmlns="http://www.w3.org/2000/svg"><rect width="12.7" height="12.7" fill="#ffc0cb" stroke-width="1.027"/><g stop-color="#000000" stroke-width=".6"><path d="m4.135 8.32-0.333-1.123h-1.383l-0.333 1.123h-0.4911l1.208-3.94h0.6265l1.208 3.94zm-0.9991-3.437h-0.0508l-0.5532 1.902h1.157z"/><path d="m5.314 8.32v-3.94h1.434q0.5475 0 0.8354 0.2992 0.2879 0.2992 0.2879 0.8354t-0.2879 0.8354q-0.2879 0.2992-0.8354 0.2992h-0.9596v1.671zm0.4741-2.083h0.9483q0.2992 0 0.4628-0.1468 0.1693-0.1524 0.1693-0.429v-0.2935q0-0.2766-0.1693-0.4233-0.1637-0.1524-0.4628-0.1524h-0.9483z"/><path d="m8.667 8.32v-0.3782h0.9821v-3.183h-0.9821v-0.3782h2.438v0.3782h-0.9821v3.183h0.9821v0.3782z"/></g></svg>

Before

Width:  |  Height:  |  Size: 785 B

View File

@@ -1,4 +0,0 @@
.library-name--link { text-transform: uppercase; }
.library-version { top: 0px; margin-left: 10px; }

View File

@@ -1,32 +1,22 @@
# suppress inspection "UnusedProperty" for whole file
kotlin.code.style=official
# For optimal compilation performance
org.gradle.jvmargs=-Xmx2G -XX:+UseParallelGC
kotlin.incremental.multiplatform=true
# https://kotlinlang.org/docs/gradle.html#check-for-jvm-target-compatibility-of-related-compile-tasks
kotlin.jvm.target.validation.mode=error
org.gradle.parallel=true
org.gradle.caching=true
#org.gradle.configuration-cache=true
#org.gradle.configuration-cache.parallel=true
#org.gradle.configuration-cache.problems=warn
# Whether to automatically bundle the Kotlin standard library (true by default)
# https://kotlinlang.org/docs/gradle.html#dependency-on-the-standard-library
kotlin.stdlib.default.dependency=true
kotlin.mpp.import.legacyTestSourceSetDetection=true
# Enable Dokka 2.0.0
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library

View File

@@ -1,30 +1,6 @@
[versions]
kotlinApi = "2.2"
kotlinLanguage = "2.2"
kotlin = "2.2.21"
jvmTarget = "17"
openrndr = { require = "[0.5,0.6.0)" }
kotest = "5.9.1"
dokka = "2.1.0"
nebulaRelease = "18.0.7"
boofcv = "1.2.4"
libfreenect = "0.5.7-1.5.9"
librealsense = "2.53.1-1.5.9"
gson = "2.13.2"
antlr = "4.13.2"
antlrKotlin = "1.0.8"
minim = "2.2.2"
netty = "4.2.7.Final"
rabbitcontrol = "0.3.39"
zxing = "3.5.4"
ktor = "3.3.2"
jgit = "7.3.0.202506031305-r"
javaosc = "0.9"
jsoup = "1.21.2"
mockk = "1.14.2"
processing = "4.4.10"
nmcp = "1.2.0"
okhttp = "5.2.1"
agp = "8.13.1"
junit = "4.13.2"
coreKtx = "1.17.0"
@@ -39,36 +15,6 @@ kotlinx-serialization = "1.9.0"
mapbox = "11.16.6"
[libraries]
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
kotlin-scriptingJvm = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-jvm", version.ref = "kotlin" }
kotlin-scriptingJvmHost = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-jvm-host", version.ref = "kotlin" }
kotlin-scriptingJSR223 = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-jsr223", version.ref = "kotlin" }
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serialization-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
dokka-gradle-plugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk"}
processing-core = { group = "org.processing", name = "core", version.ref = "processing"}
boofcv = { group = "org.boofcv", name = "boofcv-core", version.ref = "boofcv" }
libfreenect = { group = "org.bytedeco", name = "libfreenect", version.ref = "libfreenect" }
librealsense = { group = "org.bytedeco", name = "librealsense2", version.ref = "librealsense" }
minim = { group = "net.compartmental.code", name = "minim", version.ref = "minim" }
netty-all = { group = "io.netty", name = "netty-all", version.ref = "netty" }
rabbitcontrol-rcp = { group = "cc.rabbitcontrol", name = "rcp", version.ref = "rabbitcontrol" }
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
jgit = { group = "org.eclipse.jgit", name = "org.eclipse.jgit", version.ref = "jgit" }
javaosc-core = { group = "com.illposed.osc", name = "javaosc-core", version.ref = "javaosc" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
antlr-core = { group = "org.antlr", name = "antlr4", version.ref = "antlr" }
antlr-runtime = { group = "org.antlr", name = "antlr4-runtime", version.ref = "antlr" }
antlr-kotlin-runtime = { group = "com.strumenta", name = "antlr-kotlin-runtime", version.ref = "antlrKotlin" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -90,11 +36,7 @@ mapbox-maps = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapbox"
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
nebula-release = { id = "nebula.release", version.ref = "nebulaRelease" }
kotest-multiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest" }
antlr-kotlin = { id = "com.strumenta.antlr-kotlin", version.ref = "antlrKotlin" }
nmcp = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }

Binary file not shown.

5
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"

3
gradlew.bat vendored
View File

@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More