Compare commits
24 Commits
8990a6cf64
...
android-on
| Author | SHA1 | Date | |
|---|---|---|---|
| 204c8fd599 | |||
| a58486bff0 | |||
| 0c90073363 | |||
| 2525d30c80 | |||
| 0d15c60606 | |||
| ac86ab3976 | |||
| 816e954ed8 | |||
| de15029b2b | |||
| a1a9a9e0e4 | |||
| f81eee8716 | |||
|
|
3ba0395c16 | ||
|
|
10888b0e83 | ||
|
|
6024e62af0 | ||
|
|
4af2ed3fed | ||
|
|
522627ca51 | ||
|
|
72368deb85 | ||
|
|
7ad88da049 | ||
|
|
b24586288d | ||
|
|
9d68b75c5d | ||
|
|
ce123dfabd | ||
|
|
c0832197cd | ||
|
|
e21683640d | ||
|
|
97752e9cf1 | ||
|
|
987c6dafba |
23
.gitignore
vendored
@@ -1,12 +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
|
||||
162
CONTRIBUTING.md
@@ -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
@@ -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
@@ -1,120 +0,0 @@
|
||||
# ORX (OPENRNDR EXTRA)
|
||||
|
||||
[](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 | 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 | 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.
|
||||
1
android/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
62
android/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.icegps.geotools"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.icegps.geotools"
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions.jvmTarget = JvmTarget.JVM_17
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.mapbox.maps)
|
||||
implementation(project(":math"))
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(project(":icegps-common"))
|
||||
implementation(project(":icegps-shared"))
|
||||
implementation(project(":icegps-triangulation"))
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
||||
21
android/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.icegps.geotools", appContext.packageName)
|
||||
}
|
||||
}
|
||||
25
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Include this permission to grab user's general location -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- Include only if your app benefits from precise location access. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Orx">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
427
android/src/main/java/com/icegps/geotools/ContoursManager.kt
Normal file
@@ -0,0 +1,427 @@
|
||||
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.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
|
||||
import com.mapbox.geojson.Polygon
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.Style
|
||||
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
||||
import com.mapbox.maps.extension.style.layers.addLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.fillLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.lineLayer
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.LineCap
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
|
||||
import com.mapbox.maps.extension.style.sources.addSource
|
||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
|
||||
class ContoursManager(
|
||||
private val context: Context,
|
||||
private val mapView: MapView,
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
private val sourceId: String = "contours-source-id-10"
|
||||
private val layerId: String = "contours-layer-id-10"
|
||||
private val fillSourceId: String = "contours-fill-source-id-10"
|
||||
private val fillLayerId: String = "contours-fill-layer-id-10"
|
||||
private val gridSourceId: String = "grid-polygon-source-id"
|
||||
private val gridLayerId: String = "grid-polygon-layer-id"
|
||||
|
||||
private var contourSize: Int = 6
|
||||
private var heightRange: ClosedFloatingPointRange<Double> = 0.0..100.0
|
||||
private var cellSize: Double? = 10.0
|
||||
val simplePalette = SimplePalette(
|
||||
range = 0.0..100.0
|
||||
)
|
||||
|
||||
private var colors = colorBrewer2Palettes(
|
||||
numberOfColors = contourSize,
|
||||
paletteType = ColorBrewer2Type.Any
|
||||
).first().colors.reversed()
|
||||
|
||||
private var points: List<Vector3D> = emptyList()
|
||||
|
||||
private val polylineManager = PolylineManager(mapView)
|
||||
|
||||
fun updateContourSize(contourSize: Int) {
|
||||
this.contourSize = contourSize
|
||||
colors = colorBrewer2Palettes(
|
||||
numberOfColors = contourSize,
|
||||
paletteType = ColorBrewer2Type.Any
|
||||
).first().colors.reversed()
|
||||
}
|
||||
|
||||
fun updateCellSize(value: Double) {
|
||||
cellSize = value
|
||||
}
|
||||
|
||||
fun updatePoints(
|
||||
points: List<Vector3D>,
|
||||
) {
|
||||
this.points = points
|
||||
}
|
||||
|
||||
fun updateHeightRange(
|
||||
heightRange: ClosedFloatingPointRange<Double>? = null
|
||||
) {
|
||||
if (heightRange == null) {
|
||||
if (points.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val height = points.map { it.z }
|
||||
val range = height.min()..height.max()
|
||||
this.heightRange = range
|
||||
simplePalette.setRange(range)
|
||||
} else {
|
||||
this.heightRange = heightRange
|
||||
simplePalette.setRange(heightRange)
|
||||
}
|
||||
}
|
||||
|
||||
private var isGridVisible: Boolean = true
|
||||
private var _gridModel = MutableStateFlow<GridModel?>(null)
|
||||
val gridModel = _gridModel.asStateFlow()
|
||||
|
||||
fun setGridVisible(visible: Boolean) {
|
||||
if (visible != isGridVisible) {
|
||||
isGridVisible = visible
|
||||
if (visible) {
|
||||
_gridModel.value?.let { gridModel ->
|
||||
mapView.displayGridModel(
|
||||
grid = gridModel,
|
||||
sourceId = gridSourceId,
|
||||
layerId = gridLayerId,
|
||||
palette = simplePalette::palette
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
try {
|
||||
style.removeStyleLayer(gridLayerId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
if (style.styleSourceExists(gridSourceId)) {
|
||||
style.removeStyleSource(gridSourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var triangles: List<Triangle> = listOf()
|
||||
private var isTriangleVisible: Boolean = true
|
||||
|
||||
fun setTriangleVisible(visible: Boolean) {
|
||||
if (visible != isTriangleVisible) {
|
||||
isTriangleVisible = visible
|
||||
if (visible) {
|
||||
polylineManager.update(
|
||||
triangles.map {
|
||||
listOf(it.x1, it.x2, it.x3)
|
||||
.map { Vector3D(it.x, it.y, it.z) }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
polylineManager.clearContours()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
fun refresh() {
|
||||
val points = points
|
||||
if (points.size <= 3) {
|
||||
context.toast("points size ${points.size}")
|
||||
return
|
||||
}
|
||||
job?.cancel()
|
||||
scope.launch {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
val step = heightRange.endInclusive / contourSize
|
||||
val zip = (0..contourSize).map { index ->
|
||||
heightRange.start + index * step
|
||||
}.zipWithNext { a, b -> a..b }
|
||||
val area = points.area
|
||||
val triangulation = DelaunayTriangulation(points)
|
||||
val triangles = triangulation.triangles()
|
||||
val cellSize: Double = if (cellSize == null || cellSize!! < 0.1) {
|
||||
(max(triangulation.points.area.width, triangulation.points.area.height) / 50)
|
||||
} else {
|
||||
cellSize!!
|
||||
}
|
||||
scope.launch {
|
||||
val gridModel = triangulationToGrid(
|
||||
delaunator = triangulation,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
this@ContoursManager._gridModel.value = gridModel
|
||||
if (isGridVisible) mapView.displayGridModel(
|
||||
grid = gridModel,
|
||||
sourceId = gridSourceId,
|
||||
layerId = gridLayerId,
|
||||
palette = simplePalette::palette
|
||||
)
|
||||
}
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
val lineFeatures = mutableListOf<List<Feature>>()
|
||||
val features = zip.mapIndexed { index, range ->
|
||||
async {
|
||||
val contours = findContours(
|
||||
triangles = triangles,
|
||||
range = range,
|
||||
area = area,
|
||||
cellSize = cellSize
|
||||
)
|
||||
val color = colors[index].toColorInt()
|
||||
lineFeatures.add(contoursToLineFeatures(contours, color).flatten())
|
||||
contoursToPolygonFeatures(contours, color)
|
||||
}
|
||||
}.awaitAll()
|
||||
withContext(Dispatchers.Main) {
|
||||
if (false) setupLineLayer(
|
||||
style = style,
|
||||
sourceId = sourceId,
|
||||
layerId = layerId,
|
||||
features = lineFeatures.flatten()
|
||||
)
|
||||
setupFillLayer(
|
||||
style = style,
|
||||
sourceId = fillSourceId,
|
||||
layerId = fillLayerId,
|
||||
features = features.filterNotNull(),
|
||||
)
|
||||
Log.d(TAG, "refresh: 刷新完成")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findContours(
|
||||
triangles: List<Triangle>,
|
||||
range: ClosedFloatingPointRange<Double>,
|
||||
area: Rectangle,
|
||||
cellSize: Double
|
||||
): List<ShapeContour> {
|
||||
return findContours(
|
||||
f = { v ->
|
||||
val triangle = triangles.firstOrNull { triangle ->
|
||||
isPointInTriangle3D(v, listOf(triangle.x1, triangle.x2, triangle.x3))
|
||||
}
|
||||
(triangle?.let { triangle ->
|
||||
val interpolate = interpolateHeight(
|
||||
point = v,
|
||||
triangle = listOf(
|
||||
triangle.x1,
|
||||
triangle.x2,
|
||||
triangle.x3,
|
||||
)
|
||||
)
|
||||
if (interpolate.z in range) -1.0
|
||||
else 1.0
|
||||
} ?: 1.0).also {
|
||||
Log.d(TAG, "findContours: ${v} -> ${it}")
|
||||
}
|
||||
},
|
||||
area = area,
|
||||
cellSize = cellSize,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupLineLayer(
|
||||
style: Style,
|
||||
sourceId: String,
|
||||
layerId: String,
|
||||
features: List<Feature>
|
||||
) {
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
|
||||
val source = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(features))
|
||||
}
|
||||
style.addSource(source)
|
||||
|
||||
val layer = lineLayer(layerId, sourceId) {
|
||||
lineColor(Expression.toColor(Expression.Companion.get("color"))) // 从属性获取颜色
|
||||
lineWidth(1.0)
|
||||
lineCap(LineCap.ROUND)
|
||||
lineJoin(LineJoin.ROUND)
|
||||
lineOpacity(0.8)
|
||||
}
|
||||
style.addLayer(layer)
|
||||
}
|
||||
|
||||
private fun setupFillLayer(
|
||||
style: Style,
|
||||
sourceId: String,
|
||||
layerId: String,
|
||||
features: List<Feature>
|
||||
) {
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
|
||||
val source = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(features))
|
||||
}
|
||||
style.addSource(source)
|
||||
|
||||
val layer = fillLayer(layerId, sourceId) {
|
||||
fillColor(Expression.Companion.toColor(Expression.get("color"))) // 从属性获取颜色
|
||||
fillOpacity(0.5)
|
||||
fillAntialias(true)
|
||||
}
|
||||
style.addLayer(layer)
|
||||
}
|
||||
|
||||
private var useCatmullRom: Boolean = true
|
||||
|
||||
fun setCatmullRom(enabled: Boolean) {
|
||||
useCatmullRom = enabled
|
||||
}
|
||||
|
||||
fun contoursToLineFeatures(contours: List<ShapeContour>, color: Int): List<List<Feature>> {
|
||||
return contours.drop(1).map { contour ->
|
||||
contour.segments.map { segment ->
|
||||
LineString.fromLngLats(
|
||||
listOf(
|
||||
segment.start.toMapboxPoint(),
|
||||
segment.end.toMapboxPoint()
|
||||
)
|
||||
)
|
||||
}.map { lineString ->
|
||||
Feature.fromGeometry(lineString).apply {
|
||||
// 将颜色Int转换为十六进制字符串
|
||||
addStringProperty("color", color.toHexColorString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contoursToPolygonFeatures(contours: List<ShapeContour>, color: Int): Feature? {
|
||||
val lists = contours.drop(0).filter { it.segments.isNotEmpty() }.map { contour ->
|
||||
val start = contour.segments[0].start
|
||||
listOf(start) + contour.segments.map { it.end }
|
||||
}.map {
|
||||
if (!useCatmullRom) return@map it
|
||||
val cmr = CatmullRomChain2(it, 1.0, loop = true)
|
||||
val contour = ShapeContour.fromPoints(cmr.positions(200), true)
|
||||
val start = contour.segments[0].start
|
||||
listOf(start) + contour.segments.map { it.end }
|
||||
}.map { points -> points.map { it.toMapboxPoint() } }
|
||||
|
||||
if (lists.isEmpty()) {
|
||||
Log.w(TAG, "contoursToPolygonFeatures: 没有有效的轮廓数据")
|
||||
return null
|
||||
}
|
||||
|
||||
val polygon = Polygon.fromLngLats(lists)
|
||||
return Feature.fromGeometry(polygon).apply {
|
||||
// 将颜色Int转换为十六进制字符串
|
||||
addStringProperty("color", color.toHexColorString())
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toHexColorString(): String {
|
||||
return String.format("#%06X", 0xFFFFFF and this)
|
||||
}
|
||||
|
||||
fun clearContours() {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
try {
|
||||
style.removeStyleLayer(layerId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
try {
|
||||
style.removeStyleSource(sourceId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isPointInTriangle3D(point: Vector2D, triangle: List<Vector3D>): Boolean {
|
||||
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||
|
||||
val (v1, v2, v3) = triangle
|
||||
|
||||
// 计算重心坐标
|
||||
val denominator = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||
if (denominator == 0.0) return false // 退化三角形
|
||||
|
||||
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denominator
|
||||
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denominator
|
||||
val gamma = 1.0 - alpha - beta
|
||||
|
||||
// 点在三角形内当且仅当所有重心坐标都在[0,1]范围内
|
||||
return alpha >= 0 && beta >= 0 && gamma >= 0 &&
|
||||
alpha <= 1 && beta <= 1 && gamma <= 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用重心坐标计算点在三角形上的高度
|
||||
*
|
||||
* @param point 二维点 (x, y)
|
||||
* @param triangle 三角形的三个顶点
|
||||
* @return 三维点 (x, y, z)
|
||||
*/
|
||||
fun interpolateHeight(point: Vector2D, triangle: List<Vector3D>): Vector3D {
|
||||
/**
|
||||
* 计算点在三角形中的重心坐标
|
||||
*/
|
||||
fun calculateBarycentricCoordinates(
|
||||
point: Vector2D,
|
||||
v1: Vector3D,
|
||||
v2: Vector3D,
|
||||
v3: Vector3D
|
||||
): Triple<Double, Double, Double> {
|
||||
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
|
||||
|
||||
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denom
|
||||
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denom
|
||||
val gamma = 1.0 - alpha - beta
|
||||
|
||||
return Triple(alpha, beta, gamma)
|
||||
}
|
||||
|
||||
require(triangle.size == 3) { "三角形必须有3个顶点" }
|
||||
|
||||
val (v1, v2, v3) = triangle
|
||||
|
||||
// 计算重心坐标
|
||||
val (alpha, beta, gamma) = calculateBarycentricCoordinates(point, v1, v2, v3)
|
||||
|
||||
// 使用重心坐标插值z值
|
||||
val z = alpha * v1.z + beta * v2.z + gamma * v3.z
|
||||
|
||||
return Vector3D(point.x, point.y, z)
|
||||
}
|
||||
197
android/src/main/java/com/icegps/geotools/ControllableArrow.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.math.geometry.Angle
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.math.geometry.degrees
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
fun coordinateGenerate(): List<Vector3D> {
|
||||
val minX = -20.0
|
||||
val maxX = 20.0
|
||||
val minY = -20.0
|
||||
val maxY = 20.0
|
||||
val minZ = -20.0
|
||||
val maxZ = 20.0
|
||||
val x: () -> Double = { Random.nextDouble(minX, maxX) }
|
||||
val y: () -> Double = { Random.nextDouble(minY, maxY) }
|
||||
val z: () -> Double = { Random.nextDouble(minZ, maxZ) }
|
||||
val dPoints = (0..60).map {
|
||||
Vector3D(x(), y(), z())
|
||||
}
|
||||
return dPoints
|
||||
}
|
||||
|
||||
fun coordinateGenerate1(): List<List<Vector3D>> {
|
||||
/**
|
||||
* 绕 Z 轴旋转指定角度(弧度)
|
||||
*/
|
||||
fun Vector3D.rotateAroundZ(angle: Angle): Vector3D {
|
||||
val cosAngle = cos(angle.radians)
|
||||
val sinAngle = sin(angle.radians)
|
||||
|
||||
return Vector3D(
|
||||
x = x * cosAngle - y * sinAngle,
|
||||
y = x * sinAngle + y * cosAngle,
|
||||
z = z
|
||||
)
|
||||
}
|
||||
|
||||
val center = Vector3D()
|
||||
val direction = Vector3D(0.0, 1.0, -1.0)
|
||||
return (0..360).step(10).map {
|
||||
val nowDirection = direction.rotateAroundZ(it.degrees)
|
||||
listOf(2, 6, 10).map {
|
||||
center + nowDirection * it
|
||||
}
|
||||
}
|
||||
}
|
||||
144
android/src/main/java/com/icegps/geotools/DisplaySlopeResult.kt
Normal 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)
|
||||
}
|
||||
438
android/src/main/java/com/icegps/geotools/EarthworkManager.kt
Normal 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)} m³")
|
||||
appendLine("填方: ${"%.1f".format(fillVolume)} m³")
|
||||
appendLine("净土方: ${"%.1f".format(netVolume)} m³")
|
||||
appendLine("挖方面积: ${"%.1f".format(cutArea)} m²")
|
||||
appendLine("填方面积: ${"%.1f".format(fillArea)} m²")
|
||||
appendLine("总面积:${"%.1f".format(totalArea)} m²")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EarthworkManager(
|
||||
private val mapView: MapView,
|
||||
private val scope: CoroutineScope
|
||||
) {
|
||||
private val arrowSourceId: String = "controllable-source-id-0"
|
||||
private val arrowLayerId: String = "controllable-layer-id-0"
|
||||
private var listener: OnMoveListener? = null
|
||||
|
||||
private var gridModel = MutableStateFlow<GridModel?>(null)
|
||||
private val arrowHead = MutableStateFlow(emptyList<Vector2D>())
|
||||
private var arrowCenter = MutableStateFlow(Vector2D(0.0, 0.0))
|
||||
private var arrowEnd = MutableStateFlow(Vector2D(0.0, 1.0))
|
||||
private var _slopeDirection = MutableStateFlow(0.degrees)
|
||||
val slopeDirection = _slopeDirection.asStateFlow()
|
||||
private val _slopePercentage = MutableStateFlow(90.0)
|
||||
val slopePercentage = _slopePercentage.asStateFlow()
|
||||
private val _baseHeightOffset = MutableStateFlow(0.0)
|
||||
val baseHeightOffset = _baseHeightOffset.asStateFlow()
|
||||
|
||||
init {
|
||||
combine(
|
||||
arrowCenter,
|
||||
arrowEnd,
|
||||
gridModel
|
||||
) { center, arrow, gridModel ->
|
||||
gridModel?.let { gridModel ->
|
||||
// _slopeDirection.value = angle
|
||||
displayControllableArrow(gridModel, getSlopeDirection(arrow, center))
|
||||
}
|
||||
}.launchIn(scope)
|
||||
combine(
|
||||
_slopeDirection,
|
||||
gridModel
|
||||
) { slopeDirection, gridModel ->
|
||||
gridModel?.let {
|
||||
displayControllableArrow(it, slopeDirection)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getSlopeDirection(
|
||||
arrow: Vector2D,
|
||||
center: Vector2D
|
||||
): Angle {
|
||||
val direction = (arrow - center)
|
||||
val atan2 = Angle.atan2(direction.x, direction.y, Vector2D.UP)
|
||||
val angle = atan2.normalized
|
||||
return angle
|
||||
}
|
||||
|
||||
private fun displayControllableArrow(gridModel: GridModel, slopeDirection: Angle) {
|
||||
val arrowData = calculateArrowData(
|
||||
grid = gridModel,
|
||||
angle = slopeDirection,
|
||||
)
|
||||
arrowHead.value = arrowData.headRing
|
||||
mapView.displayControllableArrow(
|
||||
sourceId = arrowSourceId,
|
||||
layerId = arrowLayerId,
|
||||
arrowData = arrowData,
|
||||
)
|
||||
}
|
||||
|
||||
fun Point.toVector2D(): Vector2D {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
val enu = geoHelper.wgs84ToENU(lon = longitude(), lat = latitude(), hgt = 0.0)
|
||||
return Vector2D(enu.x, enu.y)
|
||||
}
|
||||
|
||||
fun removeOnMoveListener() {
|
||||
listener?.let(mapView.mapboxMap::removeOnMoveListener)
|
||||
listener = null
|
||||
}
|
||||
|
||||
fun setupOnMoveListener() {
|
||||
listener = object : OnMoveListener {
|
||||
private var beginning: Boolean = false
|
||||
private var isDragging: Boolean = false
|
||||
private fun getCoordinate(focalPoint: PointF): Point {
|
||||
return mapView.mapboxMap.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||
}
|
||||
|
||||
override fun onMove(detector: MoveGestureDetector): Boolean {
|
||||
val focalPoint = detector.focalPoint
|
||||
val point = mapView.mapboxMap
|
||||
.coordinateForPixel(ScreenCoordinate(focalPoint.x.toDouble(), focalPoint.y.toDouble()))
|
||||
.toVector2D()
|
||||
|
||||
val isPointInPolygon = RayCastingAlgorithm.isPointInPolygon(
|
||||
point = point,
|
||||
polygon = arrowHead.value
|
||||
)
|
||||
|
||||
if (isPointInPolygon) {
|
||||
isDragging = true
|
||||
}
|
||||
if (isDragging) {
|
||||
arrowEnd.value = point
|
||||
}
|
||||
return isDragging
|
||||
}
|
||||
|
||||
override fun onMoveBegin(detector: MoveGestureDetector) {
|
||||
Log.d(TAG, "onMoveBegin: $detector")
|
||||
beginning = true
|
||||
}
|
||||
|
||||
override fun onMoveEnd(detector: MoveGestureDetector) {
|
||||
Log.d(TAG, "onMoveEnd: $detector")
|
||||
val point = getCoordinate(detector.focalPoint)
|
||||
val arrow = point.toVector2D()
|
||||
if (beginning && isDragging) {
|
||||
arrowEnd.value = arrow
|
||||
val center = arrowCenter.value
|
||||
_slopeDirection.value = getSlopeDirection(arrow, center)
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
buildString {
|
||||
appendLine("onMoveEnd: ")
|
||||
appendLine("${point.longitude()}, ${point.latitude()}")
|
||||
}
|
||||
)
|
||||
isDragging = false
|
||||
beginning = false
|
||||
}
|
||||
}.also(mapView.mapboxMap::addOnMoveListener)
|
||||
}
|
||||
|
||||
fun updateGridModel(gridModel: GridModel) {
|
||||
this.gridModel.value = gridModel
|
||||
calculateArrowCenter(gridModel)
|
||||
}
|
||||
|
||||
private fun calculateArrowCenter(gridModel: GridModel) {
|
||||
val centerX = (gridModel.minX + gridModel.maxX) / 2
|
||||
val centerY = (gridModel.minY + gridModel.maxY) / 2
|
||||
arrowCenter.value = Vector2D(centerX, centerY)
|
||||
}
|
||||
|
||||
fun updateSlopeDirection(angle: Angle) {
|
||||
_slopeDirection.value = angle
|
||||
}
|
||||
|
||||
fun updateSlopePercentage(value: Double) {
|
||||
_slopePercentage.value = value
|
||||
}
|
||||
|
||||
fun updateDesignHeight(value: Double) {
|
||||
_baseHeightOffset.value = value
|
||||
}
|
||||
}
|
||||
83
android/src/main/java/com/icegps/geotools/GridDisplay.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.mapbox.geojson.Feature
|
||||
import com.mapbox.geojson.FeatureCollection
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.geojson.Polygon
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.extension.style.expressions.generated.Expression
|
||||
import com.mapbox.maps.extension.style.layers.addLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.FillLayer
|
||||
import com.mapbox.maps.extension.style.sources.addSource
|
||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
fun MapView.displayGridModel(
|
||||
grid: GridModel,
|
||||
sourceId: String,
|
||||
layerId: String,
|
||||
palette: (Double?) -> String,
|
||||
) {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
mapboxMap.getStyle { style ->
|
||||
val polygonFeatures = mutableListOf<Feature>()
|
||||
|
||||
val minX = grid.minX
|
||||
val maxY = grid.maxY
|
||||
val cellSize = grid.cellSize
|
||||
|
||||
for (r in 0 until grid.rows) {
|
||||
for (c in 0 until grid.cols) {
|
||||
val idx = r * grid.cols + c
|
||||
val v = grid.cells[idx] ?: continue
|
||||
|
||||
val x0 = minX + c * cellSize
|
||||
val y0 = maxY - r * cellSize
|
||||
val x1 = x0 + cellSize
|
||||
val y1 = y0 - cellSize
|
||||
|
||||
val ring = listOf(
|
||||
Vector2D(x0, y0),
|
||||
Vector2D(x1, y0),
|
||||
Vector2D(x1, y1),
|
||||
Vector2D(x0, y1),
|
||||
Vector2D(x0, y0),
|
||||
).map {
|
||||
geoHelper.enuToWGS84Object(GeoHelper.ENU(it.x, it.y))
|
||||
}.map {
|
||||
Point.fromLngLat(it.lon, it.lat)
|
||||
}
|
||||
val poly = Polygon.fromLngLats(listOf(ring))
|
||||
val polyFeature = Feature.fromGeometry(poly)
|
||||
polyFeature.addStringProperty("color", palette(v))
|
||||
polyFeature.addNumberProperty("value", v ?: -9999.0)
|
||||
polygonFeatures.add(polyFeature)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
style.removeStyleLayer(layerId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
if (style.styleSourceExists(sourceId)) {
|
||||
style.removeStyleSource(sourceId)
|
||||
}
|
||||
|
||||
val polygonSource = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(polygonFeatures))
|
||||
}
|
||||
style.addSource(polygonSource)
|
||||
|
||||
val fillLayer = FillLayer(layerId, sourceId).apply {
|
||||
fillColor(Expression.toColor(Expression.get("color")))
|
||||
fillOpacity(0.5)
|
||||
}
|
||||
style.addLayer(fillLayer)
|
||||
}
|
||||
}
|
||||
139
android/src/main/java/com/icegps/geotools/GridModel.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import com.icegps.math.geometry.Vector2D
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.triangulation.DelaunayTriangulation
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
data class GridModel(
|
||||
val minX: Double,
|
||||
val maxX: Double,
|
||||
val minY: Double,
|
||||
val maxY: Double,
|
||||
val rows: Int,
|
||||
val cols: Int,
|
||||
val cellSize: Double,
|
||||
val cells: Array<Double?>
|
||||
) {
|
||||
fun getValue(row: Int, col: Int): Double? {
|
||||
if (row !in 0..<rows || col < 0 || col >= cols) {
|
||||
return null
|
||||
}
|
||||
return cells[row * cols + col]
|
||||
}
|
||||
}
|
||||
|
||||
fun triangulationToGrid(
|
||||
delaunator: DelaunayTriangulation,
|
||||
cellSize: Double = 50.0,
|
||||
maxSidePixels: Int = 5000
|
||||
): GridModel {
|
||||
fun pointInTriangle(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D): Boolean {
|
||||
val v0x = c.x - a.x
|
||||
val v0y = c.y - a.y
|
||||
val v1x = b.x - a.x
|
||||
val v1y = b.y - a.y
|
||||
val v2x = pt.x - a.x
|
||||
val v2y = pt.y - a.y
|
||||
|
||||
val dot00 = v0x * v0x + v0y * v0y
|
||||
val dot01 = v0x * v1x + v0y * v1y
|
||||
val dot02 = v0x * v2x + v0y * v2y
|
||||
val dot11 = v1x * v1x + v1y * v1y
|
||||
val dot12 = v1x * v2x + v1y * v2y
|
||||
|
||||
val denom = dot00 * dot11 - dot01 * dot01
|
||||
if (denom == 0.0) return false
|
||||
val invDenom = 1.0 / denom
|
||||
val u = (dot11 * dot02 - dot01 * dot12) * invDenom
|
||||
val v = (dot00 * dot12 - dot01 * dot02) * invDenom
|
||||
return u >= 0 && v >= 0 && u + v <= 1
|
||||
}
|
||||
|
||||
fun barycentricInterpolateLegacy(pt: Vector2D, a: Vector3D, b: Vector3D, c: Vector3D, values: DoubleArray): Double {
|
||||
val area = { p1: Vector2D, p2: Vector3D, p3: Vector3D ->
|
||||
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
|
||||
}
|
||||
val area2 = { p1: Vector3D, p2: Vector3D, p3: Vector3D ->
|
||||
((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0
|
||||
}
|
||||
val areaTotal = area2(a, b, c)
|
||||
if (areaTotal == 0.0) return values[0]
|
||||
val wA = area(pt, b, c) / areaTotal
|
||||
val wB = area(pt, c, a) / areaTotal
|
||||
val wC = area(pt, a, b) / areaTotal
|
||||
return values[0] * wA + values[1] * wB + values[2] * wC
|
||||
}
|
||||
|
||||
|
||||
val pts = delaunator.points
|
||||
require(pts.isNotEmpty()) { "points empty" }
|
||||
|
||||
val x = pts.map { it.x }
|
||||
val y = pts.map { it.y }
|
||||
val minX = x.min()
|
||||
val maxX = x.max()
|
||||
val minY = y.min()
|
||||
val maxY = y.max()
|
||||
|
||||
val width = maxX - minX
|
||||
val height = maxY - minY
|
||||
|
||||
var cols = ceil(width / cellSize).toInt()
|
||||
var rows = ceil(height / cellSize).toInt()
|
||||
|
||||
// 防止过大
|
||||
if (cols > maxSidePixels) cols = maxSidePixels
|
||||
if (rows > maxSidePixels) rows = maxSidePixels
|
||||
|
||||
val cells = Array<Double?>(rows * cols) { null }
|
||||
|
||||
|
||||
val triangles = delaunator.triangles()
|
||||
|
||||
for (ti in 0 until triangles.size) {
|
||||
val (a, b, c) = triangles[ti]
|
||||
|
||||
val tminX = minOf(a.x, b.x, c.x)
|
||||
val tmaxX = maxOf(a.x, b.x, c.x)
|
||||
val tminY = minOf(a.y, b.y, c.y)
|
||||
val tmaxY = maxOf(a.y, b.y, c.y)
|
||||
|
||||
val colMin = ((tminX - minX) / cellSize).toInt().coerceIn(0, cols - 1)
|
||||
val colMax = ((tmaxX - minX) / cellSize).toInt().coerceIn(0, cols - 1)
|
||||
val rowMin = ((maxY - tmaxY) / cellSize).toInt().coerceIn(0, rows - 1)
|
||||
val rowMax = ((maxY - tminY) / cellSize).toInt().coerceIn(0, rows - 1)
|
||||
|
||||
val triVertexVals = doubleArrayOf(a.z, b.z, c.z)
|
||||
|
||||
for (r in rowMin..rowMax) {
|
||||
for (cIdx in colMin..colMax) {
|
||||
val centerX = minX + (cIdx + 0.5) * cellSize
|
||||
val centerY = maxY - (r + 0.5) * cellSize
|
||||
val pt = Vector2D(centerX, centerY)
|
||||
if (pointInTriangle(pt, a, b, c)) {
|
||||
val idx = r * cols + cIdx
|
||||
val valInterp = barycentricInterpolateLegacy(pt, a, b, c, triVertexVals)
|
||||
cells[idx] = valInterp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val grid = GridModel(
|
||||
minX = minX,
|
||||
minY = minY,
|
||||
maxX = maxX,
|
||||
maxY = maxY,
|
||||
rows = rows,
|
||||
cols = cols,
|
||||
cellSize = cellSize,
|
||||
cells = cells
|
||||
)
|
||||
return grid
|
||||
}
|
||||
265
android/src/main/java/com/icegps/geotools/MainActivity.kt
Normal file
@@ -0,0 +1,265 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.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
|
||||
private lateinit var mapView: MapView
|
||||
private val viewModel: MainViewModel by lazy {
|
||||
ViewModelProvider(this)[MainViewModel::class.java]
|
||||
}
|
||||
private lateinit var contoursManager: ContoursManager
|
||||
private lateinit var earthworkManager: EarthworkManager
|
||||
|
||||
init {
|
||||
initGeoHelper()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
mapView.mapboxMap.setCamera(
|
||||
CameraOptions.Builder()
|
||||
.center(Point.fromLngLat(home.longitude, home.latitude))
|
||||
.pitch(0.0)
|
||||
.zoom(18.0)
|
||||
.bearing(0.0)
|
||||
.build()
|
||||
)
|
||||
|
||||
val points = coordinateGenerate()
|
||||
|
||||
// divider
|
||||
contoursManager = ContoursManager(
|
||||
context = this,
|
||||
mapView = mapView,
|
||||
scope = lifecycleScope
|
||||
)
|
||||
contoursManager.updateContourSize(6)
|
||||
contoursManager.updatePoints(points)
|
||||
val height = points.map { it.z }
|
||||
val min = height.min()
|
||||
val max = height.max()
|
||||
contoursManager.updateHeightRange((min / 2)..max)
|
||||
binding.heightRange.values = listOf(min.toFloat() / 2, max.toFloat())
|
||||
binding.heightRange.valueFrom = min.toFloat()
|
||||
binding.heightRange.valueTo = max.toFloat()
|
||||
contoursManager.refresh()
|
||||
|
||||
binding.sliderTargetHeight.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(p0: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(p0: Slider) {
|
||||
val present = p0.value / p0.valueTo
|
||||
// val targetHeight = ((valueRange.endInclusive - valueRange.start) * present) + valueRange.start
|
||||
|
||||
// val contours = findContours(triangles, targetHeight)
|
||||
// contoursTest.clearContours()
|
||||
// if (false) contoursTest.updateContours(contours)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.heightRange.addOnSliderTouchListener(
|
||||
object : RangeSlider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: RangeSlider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: RangeSlider) {
|
||||
contoursManager.updateHeightRange((slider.values.min().toDouble() - 1.0)..(slider.values.max().toDouble() + 1.0))
|
||||
contoursManager.refresh()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.switchGrid.setOnCheckedChangeListener { _, isChecked ->
|
||||
contoursManager.setGridVisible(isChecked)
|
||||
}
|
||||
binding.switchTriangle.setOnCheckedChangeListener { _, isChecked ->
|
||||
contoursManager.setTriangleVisible(isChecked)
|
||||
}
|
||||
binding.update.setOnClickListener {
|
||||
contoursManager.refresh()
|
||||
}
|
||||
binding.cellSize.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
contoursManager.updateCellSize(slider.value.toDouble())
|
||||
contoursManager.refresh()
|
||||
}
|
||||
}
|
||||
)
|
||||
mapView.mapboxMap.addOnMapClickListener {
|
||||
viewModel.addPoint(it)
|
||||
true
|
||||
}
|
||||
binding.clearPoints.setOnClickListener {
|
||||
viewModel.clearPoints()
|
||||
}
|
||||
binding.slopeDirection.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateSlopeDirection(slider.value.degrees)
|
||||
}
|
||||
}
|
||||
)
|
||||
binding.slopePercentage.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateSlopePercentage(slider.value.toDouble())
|
||||
}
|
||||
}
|
||||
)
|
||||
binding.designHeight.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
earthworkManager.updateDesignHeight(slider.value.toDouble())
|
||||
}
|
||||
}
|
||||
)
|
||||
binding.switchDesignSurface.setOnCheckedChangeListener { button, isChecked ->
|
||||
showDesignHeight.value = isChecked
|
||||
}
|
||||
earthworkManager.setupOnMoveListener()
|
||||
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) {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
geoHelper.wgs84ToENU(
|
||||
lon = base.longitude,
|
||||
lat = base.latitude,
|
||||
hgt = base.altitude
|
||||
)
|
||||
}
|
||||
59
android/src/main/java/com/icegps/geotools/MainViewModel.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.geotools.ktx.toast
|
||||
import com.icegps.shared.SharedHttpClient
|
||||
import com.icegps.shared.SharedJson
|
||||
import com.icegps.shared.api.OpenElevation
|
||||
import com.icegps.shared.api.OpenElevationApi
|
||||
import com.icegps.shared.ktx.TAG
|
||||
import com.icegps.shared.model.GeoPoint
|
||||
import com.mapbox.geojson.Point
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class MainViewModel(private val context: Application) : AndroidViewModel(context) {
|
||||
private val geoHelper = GeoHelper.Companion.getSharedInstance()
|
||||
private val openElevation: OpenElevationApi = OpenElevation(SharedHttpClient(SharedJson()))
|
||||
|
||||
private val _points = MutableStateFlow<List<Point>>(emptyList())
|
||||
val points = _points.filter { it.size > 3 }.debounce(1000).map {
|
||||
openElevation.lookup(it.map { GeoPoint(it.longitude(), it.latitude(), it.altitude()) })
|
||||
}.catch {
|
||||
Log.e(TAG, "高程请求失败", it)
|
||||
context.toast("高程请求失败")
|
||||
}.map {
|
||||
it.map {
|
||||
val enu = geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude)
|
||||
Vector3D(enu.x, enu.y, enu.z)
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
fun addPoint(point: Point) {
|
||||
context.toast("${point.longitude()}, ${point.latitude()}")
|
||||
_points.update {
|
||||
it.toMutableList().apply {
|
||||
add(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPoints() {
|
||||
_points.value = emptyList()
|
||||
}
|
||||
}
|
||||
121
android/src/main/java/com/icegps/geotools/PolylineManager.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.graphics.Color
|
||||
import com.icegps.math.geometry.Line3D
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.icegps.geotools.ktx.toMapboxPoint
|
||||
import com.mapbox.geojson.Feature
|
||||
import com.mapbox.geojson.FeatureCollection
|
||||
import com.mapbox.geojson.LineString
|
||||
import com.mapbox.maps.MapView
|
||||
import com.mapbox.maps.Style
|
||||
import com.mapbox.maps.extension.style.layers.addLayer
|
||||
import com.mapbox.maps.extension.style.layers.generated.lineLayer
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.LineCap
|
||||
import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin
|
||||
import com.mapbox.maps.extension.style.sources.addSource
|
||||
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
|
||||
|
||||
class PolylineManager(
|
||||
private val mapView: MapView
|
||||
) {
|
||||
private val sourceId: String = "polyline-source-id-0"
|
||||
private val layerId: String = "polyline-layer-id-0"
|
||||
|
||||
fun update(
|
||||
points: List<List<Vector3D>>
|
||||
) {
|
||||
val lineStrings: List<List<Feature>> = points.map {
|
||||
val lines = fromPoints(it, true)
|
||||
lines.map {
|
||||
LineString.fromLngLats(listOf(it.a.toMapboxPoint(), it.b.toMapboxPoint()))
|
||||
}
|
||||
}.map {
|
||||
it.map { Feature.fromGeometry(it) }
|
||||
}
|
||||
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
setupLineLayer(
|
||||
style = style,
|
||||
sourceId = sourceId,
|
||||
layerId = layerId,
|
||||
features = lineStrings.flatten()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFeatures(
|
||||
features: List<Feature>
|
||||
) {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
setupLineLayer(
|
||||
style = style,
|
||||
sourceId = sourceId,
|
||||
layerId = layerId,
|
||||
features = features
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLineLayer(
|
||||
style: Style,
|
||||
sourceId: String,
|
||||
layerId: String,
|
||||
features: List<Feature>
|
||||
) {
|
||||
style.removeStyleLayer(layerId)
|
||||
style.removeStyleSource(sourceId)
|
||||
|
||||
val source = geoJsonSource(sourceId) {
|
||||
featureCollection(FeatureCollection.fromFeatures(features))
|
||||
}
|
||||
style.addSource(source)
|
||||
|
||||
val layer = lineLayer(layerId, sourceId) {
|
||||
lineColor(Color.RED)
|
||||
lineWidth(2.0)
|
||||
lineCap(LineCap.Companion.ROUND)
|
||||
lineJoin(LineJoin.Companion.ROUND)
|
||||
lineOpacity(0.8)
|
||||
}
|
||||
style.addLayer(layer)
|
||||
}
|
||||
|
||||
fun clearContours() {
|
||||
mapView.mapboxMap.getStyle { style ->
|
||||
try {
|
||||
style.removeStyleLayer(layerId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
try {
|
||||
style.removeStyleSource(sourceId)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fromPoints(
|
||||
points: List<Vector3D>,
|
||||
closed: Boolean,
|
||||
) = if (points.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
if (!closed) {
|
||||
(0 until points.size - 1).map {
|
||||
Line3D(
|
||||
points[it],
|
||||
points[it + 1]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val d = (points.last() - points.first()).length
|
||||
val usePoints = if (d > 1E-6) points else points.dropLast(1)
|
||||
(usePoints.indices).map {
|
||||
Line3D(
|
||||
usePoints[it],
|
||||
usePoints[(it + 1) % usePoints.size]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() })
|
||||
}
|
||||
}
|
||||
123
android/src/main/java/com/icegps/geotools/SimplePalette.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
class SimplePalette(
|
||||
private var range: ClosedFloatingPointRange<Double>
|
||||
) {
|
||||
fun setRange(range: ClosedFloatingPointRange<Double>) {
|
||||
this.range = range
|
||||
}
|
||||
|
||||
private val colors: Map<Int, String>
|
||||
|
||||
init {
|
||||
colors = generateTerrainColorMap()
|
||||
}
|
||||
|
||||
fun palette(value: Double?): String {
|
||||
if (value == null) return "#00000000"
|
||||
val minH = range.start
|
||||
val maxH = range.endInclusive
|
||||
val normalized = ((value - minH) / (maxH - minH)).coerceIn(0.0, 1.0)
|
||||
return colors[(normalized * 255).toInt()] ?: "#00000000"
|
||||
}
|
||||
|
||||
fun palette1(value: Double?): String {
|
||||
return if (value == null) "#00000000" else {
|
||||
// 假设您已经知道高度范围,或者动态计算
|
||||
val minH = range.start
|
||||
val maxH = range.endInclusive
|
||||
val normalized = ((value - minH) / (maxH - minH)).coerceIn(0.0, 1.0)
|
||||
val alpha = (normalized * 255).toInt()
|
||||
String.format("#%02X%02X%02X", alpha, 0, 0)
|
||||
}.also {
|
||||
Log.d("simplePalette", "$value -> $it")
|
||||
}
|
||||
}
|
||||
|
||||
fun generateGrayscaleColorMap2(): MutableMap<Int, String> {
|
||||
val colorMap = mutableMapOf<Int, String>()
|
||||
|
||||
// 定义关键灰度点
|
||||
val black = Color(0, 0, 0) // 低地势 - 黑色
|
||||
val darkGray = Color(64, 64, 64) // 过渡
|
||||
val midGray = Color(128, 128, 128) // 中间
|
||||
val lightGray = Color(192, 192, 192) // 过渡
|
||||
val white = Color(255, 255, 255) // 高地势 - 白色
|
||||
|
||||
for (i in 0..255) {
|
||||
val position = i / 255.0
|
||||
|
||||
val color = when {
|
||||
position < 0.25 -> interpolateColor(black, darkGray, position / 0.25)
|
||||
position < 0.5 -> interpolateColor(darkGray, midGray, (position - 0.25) / 0.25)
|
||||
position < 0.75 -> interpolateColor(midGray, lightGray, (position - 0.5) / 0.25)
|
||||
else -> interpolateColor(lightGray, white, (position - 0.75) / 0.25)
|
||||
}
|
||||
colorMap[i] = color.toHex()
|
||||
}
|
||||
|
||||
return colorMap
|
||||
}
|
||||
|
||||
fun generateGrayscaleColorMap(): MutableMap<Int, String> {
|
||||
val colorMap = mutableMapOf<Int, String>()
|
||||
|
||||
for (i in 0..255) {
|
||||
// 从黑色到白色的线性渐变
|
||||
val grayValue = i
|
||||
val color = Color(grayValue, grayValue, grayValue)
|
||||
colorMap[i] = color.toHex()
|
||||
}
|
||||
|
||||
return colorMap
|
||||
}
|
||||
|
||||
fun generateTerrainColorMap(): MutableMap<Int, String> {
|
||||
val colorMap = mutableMapOf<Int, String>()
|
||||
|
||||
// 定义关键颜色点
|
||||
val blue = Color(0, 0, 255) // 低地势 - 蓝色
|
||||
val cyan = Color(0, 255, 255) // 中间过渡
|
||||
val green = Color(0, 255, 0) // 中间过渡
|
||||
val yellow = Color(255, 255, 0) // 中间过渡
|
||||
val red = Color(255, 0, 0) // 高地势 - 红色
|
||||
|
||||
for (i in 0..255) {
|
||||
val position = i / 255.0
|
||||
|
||||
val color = when {
|
||||
position < 0.25 -> interpolateColor(blue, cyan, position / 0.25)
|
||||
position < 0.5 -> interpolateColor(cyan, green, (position - 0.25) / 0.25)
|
||||
position < 0.75 -> interpolateColor(green, yellow, (position - 0.5) / 0.25)
|
||||
else -> interpolateColor(yellow, red, (position - 0.75) / 0.25)
|
||||
}
|
||||
colorMap[i] = color.toHex()
|
||||
}
|
||||
|
||||
return colorMap
|
||||
}
|
||||
|
||||
fun interpolateColor(start: Color, end: Color, fraction: Double): Color {
|
||||
val r = (start.red + (end.red - start.red) * fraction).toInt()
|
||||
val g = (start.green + (end.green - start.green) * fraction).toInt()
|
||||
val b = (start.blue + (end.blue - start.blue) * fraction).toInt()
|
||||
return Color(r, g, b)
|
||||
}
|
||||
|
||||
// Color类简化实现
|
||||
class Color(val red: Int, val green: Int, val blue: Int) {
|
||||
fun toArgb(): Int {
|
||||
return (0xFF shl 24) or (red shl 16) or (green shl 8) or blue
|
||||
}
|
||||
|
||||
fun toHex(): String {
|
||||
return String.format("#%06X", toArgb() and 0xFFFFFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Catmull–Rom spline for a chain of points and returns the combined curve.
|
||||
*
|
||||
* For more details, see [CatmullRom2].
|
||||
*
|
||||
* @param points The [List] of 2D points where [CatmullRom2] is applied in groups of 4.
|
||||
* @param alpha The *tension* of the curve.
|
||||
* Use `0.0` for the uniform spline, `0.5` for the centripetal spline, `1.0` for the chordal spline.
|
||||
* @param loop Whether to connect the first and last point, such that it forms a closed shape.
|
||||
*/
|
||||
class CatmullRomChain2(points: List<Vector2D>, alpha: Double = 0.5, val loop: Boolean = false) {
|
||||
val segments = if (!loop) {
|
||||
val startPoints = points.take(2)
|
||||
val endPoints = points.takeLast(2)
|
||||
val mirrorStart =
|
||||
startPoints.first() - (startPoints.last() - startPoints.first()).normalized
|
||||
val mirrorEnd = endPoints.last() + (endPoints.last() - endPoints.first()).normalized
|
||||
|
||||
(listOf(mirrorStart) + points + listOf(mirrorEnd)).windowed(4, 1).map {
|
||||
CatmullRom2(it[0], it[1], it[2], it[3], alpha)
|
||||
}
|
||||
} else {
|
||||
val cleanPoints = if (loop && points.first().distanceTo(points.last()) <= 1.0E-6) {
|
||||
points.dropLast(1)
|
||||
} else {
|
||||
points
|
||||
}
|
||||
(cleanPoints + cleanPoints.take(3)).windowed(4, 1).map {
|
||||
CatmullRom2(it[0], it[1], it[2], it[3], alpha)
|
||||
}
|
||||
}
|
||||
|
||||
fun positions(steps: Int = segments.size * 4): List<Vector2D> {
|
||||
return (0..steps).map {
|
||||
position(it.toDouble() / steps)
|
||||
}
|
||||
}
|
||||
|
||||
fun position(rt: Double): Vector2D {
|
||||
val st = if (loop) rt.mod(1.0) else rt.coerceIn(0.0, 1.0)
|
||||
val segmentIndex = (min(almostOne, st) * segments.size).toInt()
|
||||
val t = (min(almostOne, st) * segments.size) - segmentIndex
|
||||
return segments[segmentIndex].position(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<Vector2D>.catmullRom(alpha: Double = 0.5, closed: Boolean) = CatmullRomChain2(this, alpha, closed)
|
||||
|
||||
/** Converts spline to a [Segment]. */
|
||||
fun CatmullRom2.toSegment(): Segment2D {
|
||||
val d1a2 = (p1 - p0).length.pow(2 * alpha)
|
||||
val d2a2 = (p2 - p1).length.pow(2 * alpha)
|
||||
val d3a2 = (p3 - p2).length.pow(2 * alpha)
|
||||
val d1a = (p1 - p0).length.pow(alpha)
|
||||
val d2a = (p2 - p1).length.pow(alpha)
|
||||
val d3a = (p3 - p2).length.pow(alpha)
|
||||
|
||||
val b0 = p1
|
||||
val b1 = (p2 * d1a2 - p0 * d2a2 + p1 * (2 * d1a2 + 3 * d1a * d2a + d2a2)) / (3 * d1a * (d1a + d2a))
|
||||
val b2 = (p1 * d3a2 - p3 * d2a2 + p2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
|
||||
val b3 = p2
|
||||
|
||||
return Segment2D(b0, b1, b2, b3)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts chain to a [ShapeContour].
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun CatmullRomChain2.toContour(): ShapeContour =
|
||||
ShapeContour(segments.map { it.toSegment() }, this.loop)
|
||||
427
android/src/main/java/com/icegps/geotools/color/ColorRGBa.kt
Normal 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)
|
||||
@@ -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
|
||||
23
android/src/main/java/com/icegps/geotools/ktx/ColorRGBa.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import com.icegps.geotools.color.ColorRGBa
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
fun ColorRGBa.toColorInt(): Int {
|
||||
val clampedR = r.coerceIn(0.0, 1.0)
|
||||
val clampedG = g.coerceIn(0.0, 1.0)
|
||||
val clampedB = b.coerceIn(0.0, 1.0)
|
||||
val clampedAlpha = alpha.coerceIn(0.0, 1.0)
|
||||
|
||||
return ((clampedAlpha * 255).toInt() shl 24) or
|
||||
((clampedR * 255).toInt() shl 16) or
|
||||
((clampedG * 255).toInt() shl 8) or
|
||||
((clampedB * 255).toInt())
|
||||
}
|
||||
|
||||
fun ColorRGBa.toColorHex(): String {
|
||||
return String.format("#%06X", 0xFFFFFF and toColorInt())
|
||||
}
|
||||
12
android/src/main/java/com/icegps/geotools/ktx/Context.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
/**
|
||||
* @author tabidachinokaze
|
||||
* @date 2025/11/25
|
||||
*/
|
||||
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Toast.makeText(this, text, duration).show()
|
||||
}
|
||||
24
android/src/main/java/com/icegps/geotools/ktx/Vector2D.kt
Normal 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
|
||||
32
android/src/main/java/com/icegps/geotools/ktx/Vector3D.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.icegps.geotools.ktx
|
||||
|
||||
import com.icegps.common.helper.GeoHelper
|
||||
import com.icegps.math.geometry.Rectangle
|
||||
import com.icegps.math.geometry.Vector3D
|
||||
import com.mapbox.geojson.Point
|
||||
|
||||
fun Vector3D.niceStr(): String {
|
||||
return "[$x, $y, $z]".format(this)
|
||||
}
|
||||
|
||||
fun List<Vector3D>.niceStr(): String {
|
||||
return joinToString(", ", "[", "]") {
|
||||
it.niceStr()
|
||||
}
|
||||
}
|
||||
|
||||
fun Vector3D.toMapboxPoint(): Point {
|
||||
val geoHelper = GeoHelper.getSharedInstance()
|
||||
return geoHelper.enuToWGS84Object(GeoHelper.ENU(x, y, z)).run {
|
||||
Point.fromLngLat(lon, lat, hgt)
|
||||
}
|
||||
}
|
||||
|
||||
val List<Vector3D>.area: Rectangle
|
||||
get() {
|
||||
val minX = minOf { it.x }
|
||||
val maxX = maxOf { it.x }
|
||||
val minY = minOf { it.y }
|
||||
val maxY = maxOf { it.y }
|
||||
return Rectangle(x = minX, y = minY, width = maxX - minX, height = maxY - minY)
|
||||
}
|
||||
@@ -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) {
|
||||
170
android/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
android/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
190
android/src/main/res/layout-port/activity_main.xml
Normal 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>
|
||||
186
android/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,186 @@
|
||||
<?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="horizontal"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="32dp">
|
||||
|
||||
<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>
|
||||
|
||||
<com.mapbox.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3" />
|
||||
</LinearLayout>
|
||||
6
android/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
android/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
android/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
android/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
7
android/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.Orx" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
</style>
|
||||
</resources>
|
||||
5
android/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
4
android/src/main/res/values/mapbox_access_token.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="mapbox_access_token" translatable="false" tools:ignore="UnusedResources">pk.eyJ1IjoienpxMSIsImEiOiJjbWYzbzV1MzQwMHJvMmpvbG1wbjJwdjUyIn0.LvKjIrCv9dAFcGxOM52f2Q</string>
|
||||
</resources>
|
||||
3
android/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">icegps-orx</string>
|
||||
</resources>
|
||||
9
android/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.Orx" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
|
||||
<style name="Theme.Orx" parent="Base.Theme.Orx" />
|
||||
</resources>
|
||||
17
android/src/test/java/com/icegps/geotools/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.icegps.geotools
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
|
||||
|
|
||||
|
|
||||
|[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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package org.openrndr.extra.convention
|
||||
|
||||
addHostMachineAttributesToRuntimeConfigurations()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.openrndr.extra.convention
|
||||
|
||||
plugins {
|
||||
id("orx-variant")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("orxVariants") {
|
||||
id = "orx-variant"
|
||||
implementationClass = "org.openrndr.extra.variant.plugin.VariantPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#/bin/bash
|
||||
xvfb-run -e /dev/stdout java "$@"
|
||||
142
build.gradle
@@ -1,140 +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" + " " * 36 + " | description |")
|
||||
newReadme.add("| --- | --- |")
|
||||
newReadme.addAll(orxMultiplatform)
|
||||
|
||||
newReadme.add("\n## JVM only\n")
|
||||
newReadme.add("| name" + " " * 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}\"")
|
||||
}
|
||||
1
demo-data/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
**exported*
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 577 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 840 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 826 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,2 +0,0 @@
|
||||
# Blender 4.1.1 MTL File: 'None'
|
||||
# www.blender.org
|
||||
@@ -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/
|
||||
@@ -1,14 +0,0 @@
|
||||
smiling
|
||||
dumb
|
||||
happy
|
||||
red
|
||||
green
|
||||
blue
|
||||
pink
|
||||
white
|
||||
black
|
||||
purple
|
||||
crying
|
||||
running
|
||||
jumping
|
||||
eating
|
||||
@@ -1,14 +0,0 @@
|
||||
man
|
||||
woman
|
||||
child
|
||||
boy
|
||||
girl
|
||||
house
|
||||
flower
|
||||
chair
|
||||
table
|
||||
car
|
||||
ground
|
||||
garden
|
||||
airplane
|
||||
school
|
||||
@@ -1,14 +0,0 @@
|
||||
in
|
||||
under
|
||||
at
|
||||
on
|
||||
near
|
||||
amongst
|
||||
in front of
|
||||
behind
|
||||
below
|
||||
next to
|
||||
on top of
|
||||
opposite
|
||||
with
|
||||
close to
|
||||