diff --git a/.gitignore b/.gitignore
index 185b1a64..95af1b44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ gradle.properties
/ShaderError.glsl
/.kotlin
/.lwjgl
+/local.properties
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 00000000..642ae2c5
--- /dev/null
+++ b/android/build.gradle.kts
@@ -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.orx"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ applicationId = "com.icegps.orx"
+ 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(project(":orx-triangulation"))
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(project(":icegps-common"))
+ implementation(project(":icegps-shared"))
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/android/src/androidTest/java/com/icegps/orx/ExampleInstrumentedTest.kt b/android/src/androidTest/java/com/icegps/orx/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..2973c094
--- /dev/null
+++ b/android/src/androidTest/java/com/icegps/orx/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.icegps.orx
+
+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.orx", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d92b7ef5
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/java/com/icegps/orx/MainActivity.kt b/android/src/main/java/com/icegps/orx/MainActivity.kt
new file mode 100644
index 00000000..727e01d0
--- /dev/null
+++ b/android/src/main/java/com/icegps/orx/MainActivity.kt
@@ -0,0 +1,77 @@
+package com.icegps.orx
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.icegps.common.helper.GeoHelper
+import com.icegps.orx.databinding.ActivityMainBinding
+import com.icegps.shared.SharedHttpClient
+import com.icegps.shared.SharedJson
+import com.icegps.shared.api.OpenElevation
+import com.icegps.shared.api.OpenElevationApi
+import com.icegps.shared.ktx.TAG
+import com.icegps.shared.model.GeoPoint
+import com.mapbox.geojson.Point
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
+import org.openrndr.extra.triangulation.DelaunayTriangulation
+import org.openrndr.math.Vector2
+import org.openrndr.math.Vector3
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ 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
+ }
+ }
+}
+
+class MainViewModel : ViewModel() {
+ private val geoHelper = GeoHelper.getSharedInstance()
+ private val openElevation: OpenElevationApi = OpenElevation(SharedHttpClient(SharedJson()))
+
+ private val _points = MutableStateFlow>(emptyList())
+
+ init {
+ _points.map {
+ openElevation.lookup(it.map { GeoPoint(it.longitude(), it.latitude(), it.altitude()) })
+ }.catch {
+ Log.e(TAG, "高程请求失败", it)
+ }.map {
+ it.map {
+ val enu =
+ geoHelper.wgs84ToENU(lon = it.longitude, lat = it.latitude, hgt = it.altitude)
+ Vector2(enu.x, enu.y)
+ }
+ }.onEach {
+ val triangulation = DelaunayTriangulation(it)
+ triangulation.triangles().map {
+ it.contour
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ fun addPoint(point: Point) {
+ _points.update {
+ it.toMutableList().apply {
+ add(point)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/res/drawable/ic_launcher_background.xml b/android/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/android/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/res/drawable/ic_launcher_foreground.xml b/android/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/android/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/layout/activity_main.xml b/android/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..86a5d977
--- /dev/null
+++ b/android/src/main/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/mipmap-anydpi/ic_launcher.xml b/android/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/android/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/android/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/android/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/android/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/android/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/android/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/src/main/res/values-night/themes.xml b/android/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..e19e9124
--- /dev/null
+++ b/android/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml
new file mode 100644
index 00000000..c8524cd9
--- /dev/null
+++ b/android/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/android/src/main/res/values/mapbox_access_token.xml b/android/src/main/res/values/mapbox_access_token.xml
new file mode 100644
index 00000000..1a4ff3a3
--- /dev/null
+++ b/android/src/main/res/values/mapbox_access_token.xml
@@ -0,0 +1,4 @@
+
+
+ pk.eyJ1IjoienpxMSIsImEiOiJjbWYzbzV1MzQwMHJvMmpvbG1wbjJwdjUyIn0.LvKjIrCv9dAFcGxOM52f2Q
+
\ No newline at end of file
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
new file mode 100644
index 00000000..0c8c773f
--- /dev/null
+++ b/android/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ icegps-orx
+
\ No newline at end of file
diff --git a/android/src/main/res/values/themes.xml b/android/src/main/res/values/themes.xml
new file mode 100644
index 00000000..f31f908a
--- /dev/null
+++ b/android/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/test/java/com/icegps/orx/ExampleUnitTest.kt b/android/src/test/java/com/icegps/orx/ExampleUnitTest.kt
new file mode 100644
index 00000000..e7a921d3
--- /dev/null
+++ b/android/src/test/java/com/icegps/orx/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.icegps.orx
+
+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)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index ccdf6571..c2a58d8c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,10 @@ 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 {
diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts
new file mode 100644
index 00000000..59f68cf5
--- /dev/null
+++ b/desktop/build.gradle.kts
@@ -0,0 +1,36 @@
+plugins {
+ id("org.openrndr.extra.convention.kotlin-multiplatform")
+}
+
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ api(openrndr.math)
+ api(openrndr.shape)
+ implementation(project(":orx-noise"))
+ }
+ }
+ val commonTest by getting {
+ dependencies {
+ implementation(project(":orx-shapes"))
+ implementation(openrndr.shape)
+ }
+ }
+
+ val jvmDemo by getting {
+ dependencies {
+ implementation(project(":orx-triangulation"))
+ implementation(project(":orx-shapes"))
+ implementation(project(":orx-noise"))
+ implementation(openrndr.shape)
+ implementation(project(":math"))
+ implementation(project(":orx-camera"))
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
+ implementation(project(":orx-marching-squares"))
+ implementation(project(":orx-text-writer"))
+ implementation(project(":orx-obj-loader"))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/desktop/src/jvmDemo/kotlin/DemoDelaunay03.kt b/desktop/src/jvmDemo/kotlin/DemoDelaunay03.kt
new file mode 100644
index 00000000..81c28cd6
--- /dev/null
+++ b/desktop/src/jvmDemo/kotlin/DemoDelaunay03.kt
@@ -0,0 +1,605 @@
+import com.icegps.math.geometry.Angle
+import com.icegps.math.geometry.Vector3D
+import com.icegps.math.geometry.degrees
+import org.openrndr.KEY_ARROW_DOWN
+import org.openrndr.KEY_ARROW_UP
+import org.openrndr.application
+import org.openrndr.color.ColorRGBa
+import org.openrndr.draw.TextSettingMode
+import org.openrndr.draw.loadFont
+import org.openrndr.extra.camera.Camera2D
+import org.openrndr.extra.marchingsquares.findContours
+import org.openrndr.extra.noise.gradientPerturbFractal
+import org.openrndr.extra.noise.simplex
+import org.openrndr.extra.textwriter.writer
+import org.openrndr.extra.triangulation.DelaunayTriangulation
+import org.openrndr.math.Vector2
+import org.openrndr.math.Vector3
+import org.openrndr.shape.Segment2D
+import org.openrndr.shape.Segment3D
+import org.openrndr.shape.ShapeContour
+import kotlin.math.absoluteValue
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.random.Random
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+fun main() = application {
+ configure {
+ width = 720
+ height = 720
+ title = "Delaunator"
+ }
+ program {
+ val points3D = (0 until height).step(36).map { y ->
+ (0 until width).step(36).map { x ->
+ gradientPerturbFractal(
+ 300,
+ frequency = 0.8,
+ position = Vector3(x.toDouble(), y.toDouble(), seconds)
+ )
+ }
+ }.flatten().map {
+ it.copy(z = it.z * 100)
+ }
+ /*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
+ width = width,
+ height = height,
+ volcanoCount = 3
+ )*/
+ // val points3D = coordinateGenerate(width, height)
+ val zs = points3D.map { it.z }
+ println("zs = ${zs}")
+ val associate: MutableMap = points3D.associate {
+ Vector2(it.x, it.y) to it.z
+ }.toMutableMap()
+ val delaunay = DelaunayTriangulation(associate.map { it.key })
+
+ //println(points3D.niceStr())
+ extend(Camera2D())
+ println("draw")
+ var targetHeight: Double = zs.average()
+ var logEnabled = true
+ var useInterpolation = false
+ var sampleLinear = false
+ keyboard.keyDown.listen {
+ logEnabled = true
+ println(it)
+ when (it.key) {
+ KEY_ARROW_UP -> targetHeight++
+ KEY_ARROW_DOWN -> targetHeight--
+ 73 -> useInterpolation = !useInterpolation
+ 83 -> sampleLinear = !sampleLinear
+ }
+ }
+ extend {
+ val triangles = delaunay.triangles()
+ val segments = mutableListOf()
+ drawer.clear(ColorRGBa.BLACK)
+ val indexDiff = (frameCount / 1000) % triangles.size
+ for ((i, triangle) in triangles.withIndex()) {
+ val segment2DS = triangle.contour.segments.filter {
+ val startZ = associate[it.start]!!
+ val endZ = associate[it.end]!!
+ if (startZ < endZ) {
+ targetHeight in startZ..endZ
+ } else {
+ targetHeight in endZ..startZ
+ }
+ }
+
+ if (segment2DS.size == 2) {
+ val vector2s = segment2DS.map {
+ val startZ = associate[it.start]!!
+ val endZ = associate[it.end]!!
+ val start = Vector3(it.start.x, it.start.y, startZ)
+ val end = Vector3(it.end.x, it.end.y, endZ)
+ if (startZ < endZ) {
+ start to end
+ } else {
+ end to start
+ }
+ }.map { (start, end) ->
+ val segment3D = Segment3D(start, end)
+ val vector3 =
+ segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
+ vector3
+ }.map {
+ associate[it.xy] = it.z
+ it.xy
+ }
+ val element = Segment2D(vector2s[0], vector2s[1])
+ segments.add(element)
+ }
+ drawer.fill = if (indexDiff == i) {
+ ColorRGBa.CYAN
+ } else {
+ ColorRGBa.PINK.shade(1.0 - i / (triangles.size * 1.2))
+ }
+ drawer.stroke = ColorRGBa.PINK.shade(i / (triangles.size * 1.0) + 0.1)
+ drawer.contour(triangle.contour)
+ }
+
+ val sorted = connectAllSegments(segments)
+
+ drawer.stroke = ColorRGBa.WHITE
+ drawer.strokeWeight = 2.0
+ if (logEnabled) {
+ segments.forEach {
+ println("${it.start} -> ${it.end}")
+ }
+ println("=====")
+ }
+
+ sorted.forEach {
+ it.forEach {
+ if (logEnabled) println("${it.start} -> ${it.end}")
+ drawer.lineSegment(it.start, it.end)
+ drawer.fill = ColorRGBa.WHITE
+ }
+ if (logEnabled) println("=")
+ drawer.fill = ColorRGBa.YELLOW
+ if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
+ }
+ /*for (y in 0 until (area.height / cellSize).toInt()) {
+ for (x in 0 until (area.width / cellSize).toInt()) {
+ values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
+ }
+ }*/
+ val contours = findContours(
+ f = {
+ val triangle = triangles.firstOrNull { triangle ->
+ isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
+ }
+ triangle ?: return@findContours 0.0
+ val interpolate = interpolateHeight(
+ point = it,
+ triangle = listOf(
+ triangle.x1,
+ triangle.x2,
+ triangle.x3,
+ ).map {
+ Vector3(it.x, it.y, associate[it]!!)
+ }
+ )
+ interpolate.z - targetHeight
+ },
+ area = drawer.bounds,
+ cellSize = 4.0,
+ useInterpolation = useInterpolation
+ )
+ if (logEnabled) println("useInterpolation = $useInterpolation")
+ drawer.stroke = null
+ contours.forEach {
+ drawer.fill = ColorRGBa.GREEN.opacify(0.1)
+ drawer.contour(if (sampleLinear) it.sampleLinear() else it)
+
+ }
+
+ drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
+ writer {
+ drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
+ text(targetHeight.toString())
+ }
+ logEnabled = false
+ }
+ }
+}
+
+/**
+ * 射线法判断点是否在单个三角形内
+ */
+fun isPointInTriangle(point: Vector2, triangle: List): Boolean {
+ require(triangle.size == 3) { "三角形必须有3个顶点" }
+
+ val (v1, v2, v3) = triangle
+
+ // 计算重心坐标
+ val denominator = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
+ if (denominator == 0.0) return false // 退化三角形
+
+ val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denominator
+ val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denominator
+ val gamma = 1.0 - alpha - beta
+
+ // 点在三角形内当且仅当所有重心坐标都在[0,1]范围内
+ return alpha >= 0 && beta >= 0 && gamma >= 0 &&
+ alpha <= 1 && beta <= 1 && gamma <= 1
+}
+
+/**
+ * 使用重心坐标计算点在三角形上的高度
+ * @param point 二维点 (x, y)
+ * @param triangle 三角形的三个顶点
+ * @return 三维点 (x, y, z)
+ */
+fun interpolateHeight(point: Vector2, triangle: List): Vector3 {
+ require(triangle.size == 3) { "三角形必须有3个顶点" }
+
+ val (v1, v2, v3) = triangle
+
+ // 计算重心坐标
+ val (alpha, beta, gamma) = calculateBarycentricCoordinates(point, v1, v2, v3)
+
+ // 使用重心坐标插值z值
+ val z = alpha * v1.z + beta * v2.z + gamma * v3.z
+
+ return Vector3(point.x, point.y, z)
+}
+
+/**
+ * 计算点在三角形中的重心坐标
+ */
+fun calculateBarycentricCoordinates(
+ point: Vector2,
+ v1: Vector3,
+ v2: Vector3,
+ v3: Vector3
+): Triple {
+ val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
+
+ val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denom
+ val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denom
+ val gamma = 1.0 - alpha - beta
+
+ return Triple(alpha, beta, gamma)
+}
+
+fun connectAllSegments(segments: List): List> {
+ val remaining = segments.toMutableList()
+ val allPaths = mutableListOf>()
+
+ while (remaining.isNotEmpty()) {
+ val path = mutableListOf()
+
+ // 开始新路径
+ path.add(remaining.removeAt(0))
+
+ var changed: Boolean
+ do {
+ changed = false
+
+ // 向前扩展
+ val lastEnd = path.last().end
+ val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
+ if (forwardSegment != null) {
+ val connectedSegment = if (forwardSegment.start == lastEnd) {
+ forwardSegment // 正向
+ } else {
+ Segment2D(forwardSegment.end, forwardSegment.start) // 反向
+ }
+ path.add(connectedSegment)
+ remaining.remove(forwardSegment)
+ changed = true
+ }
+
+ // 向后扩展
+ val firstStart = path.first().start
+ val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
+ if (backwardSegment != null) {
+ val connectedSegment = if (backwardSegment.end == firstStart) {
+ backwardSegment // 正向
+ } else {
+ Segment2D(backwardSegment.end, backwardSegment.start) // 反向
+ }
+ path.add(0, connectedSegment)
+ remaining.remove(backwardSegment)
+ changed = true
+ }
+
+ } while (changed && remaining.isNotEmpty())
+
+ allPaths.add(path)
+ }
+
+ return allPaths
+}
+
+fun connectSegmentsEfficient(segments: List): List {
+ if (segments.isEmpty()) return emptyList()
+
+ val remaining = segments.toMutableList()
+ val connected = mutableListOf()
+
+ // 构建端点查找表
+ val startMap = mutableMapOf>()
+ val endMap = mutableMapOf>()
+
+ segments.forEach { segment ->
+ startMap.getOrPut(segment.start) { mutableListOf() }.add(segment)
+ endMap.getOrPut(segment.end) { mutableListOf() }.add(segment)
+ }
+
+ // 从第一个线段开始
+ var currentSegment = remaining.removeAt(0)
+ connected.add(currentSegment)
+
+ // 更新查找表
+ startMap[currentSegment.start]?.remove(currentSegment)
+ endMap[currentSegment.end]?.remove(currentSegment)
+
+ // 向前连接
+ while (true) {
+ val nextFromStart = startMap[currentSegment.end]?.firstOrNull()
+ val nextFromEnd = endMap[currentSegment.end]?.firstOrNull()
+
+ when {
+ nextFromStart != null -> {
+ // 正向连接
+ connected.add(nextFromStart)
+ remaining.remove(nextFromStart)
+ startMap[nextFromStart.start]?.remove(nextFromStart)
+ endMap[nextFromStart.end]?.remove(nextFromStart)
+ currentSegment = nextFromStart
+ }
+
+ nextFromEnd != null -> {
+ // 反向连接
+ val reversed = Segment2D(nextFromEnd.end, nextFromEnd.start)
+ connected.add(reversed)
+ remaining.remove(nextFromEnd)
+ startMap[nextFromEnd.start]?.remove(nextFromEnd)
+ endMap[nextFromEnd.end]?.remove(nextFromEnd)
+ currentSegment = reversed
+ }
+
+ else -> break
+ }
+ }
+
+ // 向后连接
+ currentSegment = connected.first()
+ while (true) {
+ val prevFromEnd = endMap[currentSegment.start]?.firstOrNull()
+ val prevFromStart = startMap[currentSegment.start]?.firstOrNull()
+
+ when {
+ prevFromEnd != null -> {
+ // 正向连接到开头
+ connected.add(0, prevFromEnd)
+ remaining.remove(prevFromEnd)
+ startMap[prevFromEnd.start]?.remove(prevFromEnd)
+ endMap[prevFromEnd.end]?.remove(prevFromEnd)
+ currentSegment = prevFromEnd
+ }
+
+ prevFromStart != null -> {
+ // 反向连接到开头
+ val reversed = Segment2D(prevFromStart.end, prevFromStart.start)
+ connected.add(0, reversed)
+ remaining.remove(prevFromStart)
+ startMap[prevFromStart.start]?.remove(prevFromStart)
+ endMap[prevFromStart.end]?.remove(prevFromStart)
+ currentSegment = reversed
+ }
+
+ else -> break
+ }
+ }
+
+ return connected
+}
+
+fun connectSegments(segments: List): List {
+ if (segments.isEmpty()) return emptyList()
+
+ val remaining = segments.toMutableList()
+ val connected = mutableListOf()
+
+ // 从第一个线段开始,保持原方向
+ connected.add(remaining.removeAt(0))
+
+ while (remaining.isNotEmpty()) {
+ val lastEnd = connected.last().end
+ var found = false
+
+ // 查找可以连接的线段
+ for (i in remaining.indices) {
+ val segment = remaining[i]
+
+ // 检查四种可能的连接方式
+ when {
+ // 正向连接:当前终点 == 线段起点
+ segment.start == lastEnd -> {
+ connected.add(segment)
+ remaining.removeAt(i)
+ found = true
+ break
+ }
+ // 反向连接:当前终点 == 线段终点,需要反转线段
+ segment.end == lastEnd -> {
+ connected.add(Segment2D(segment.end, segment.start)) // 反转
+ remaining.removeAt(i)
+ found = true
+ break
+ }
+ // 正向连接另一端:当前起点 == 线段终点,需要插入到前面
+ segment.end == connected.first().start -> {
+ connected.add(0, Segment2D(segment.end, segment.start)) // 反转后插入开头
+ remaining.removeAt(i)
+ found = true
+ break
+ }
+ // 反向连接另一端:当前起点 == 线段起点,需要反转并插入到前面
+ segment.start == connected.first().start -> {
+ connected.add(0, segment) // 直接插入开头(已经是正确方向)
+ remaining.removeAt(i)
+ found = true
+ break
+ }
+ }
+ }
+
+ if (!found) break // 无法找到连接线段
+ }
+
+ return connected
+}
+
+fun calculatePositionRatio(value: Double, rangeStart: Double, rangeEnd: Double): Double {
+ if (rangeStart == rangeEnd) return 0.0 // 避免除零
+
+ val ratio = (value - rangeStart) / (rangeEnd - rangeStart)
+ return ratio.coerceIn(0.0, 1.0)
+}
+
+fun sortLinesEfficient(lines: List): List {
+ if (lines.isEmpty()) return emptyList()
+
+ // 创建起点到线段的映射
+ val startMap = lines.associateBy { it.start }
+ val sorted = mutableListOf()
+
+ // 找到起点(没有其他线段的终点指向它的起点)
+ var currentLine = lines.firstOrNull { line ->
+ lines.none { it.end == line.start }
+ } ?: lines.first()
+
+ sorted.add(currentLine)
+
+ while (true) {
+ val nextLine = startMap[currentLine.end]
+ if (nextLine == null || nextLine == lines.first()) break
+ sorted.add(nextLine)
+ currentLine = nextLine
+ }
+
+ return sorted
+}
+
+fun sortLines(lines: List): List {
+ if (lines.isEmpty()) return emptyList()
+
+ val remaining = lines.toMutableList()
+ val sorted = mutableListOf()
+
+ // 从第一个线段开始
+ sorted.add(remaining.removeAt(0))
+
+ while (remaining.isNotEmpty()) {
+ val lastEnd = sorted.last().end
+ var found = false
+
+ // 查找下一个线段
+ for (i in remaining.indices) {
+ if (remaining[i].start == lastEnd) {
+ sorted.add(remaining.removeAt(i))
+ found = true
+ break
+ }
+ }
+
+ if (!found) break // 无法找到下一个线段
+ }
+
+ return sorted
+}
+
+fun findLineLoops(lines: List): List> {
+ val remaining = lines.toMutableList()
+ val loops = mutableListOf>()
+
+ while (remaining.isNotEmpty()) {
+ val loop = findSingleLoop(remaining)
+ if (loop.isNotEmpty()) {
+ loops.add(loop)
+ // 移除已使用的线段
+ loop.forEach { line ->
+ remaining.remove(line)
+ }
+ } else {
+ // 无法形成环的线段
+ break
+ }
+ }
+
+ return loops
+}
+
+fun findSingleLoop(remaining: MutableList): List {
+ if (remaining.isEmpty()) return emptyList()
+
+ val loop = mutableListOf()
+ loop.add(remaining.removeAt(0))
+
+ // 向前查找连接
+ while (remaining.isNotEmpty()) {
+ val lastEnd = loop.last().end
+ val nextIndex = remaining.indexOfFirst { it.start == lastEnd }
+
+ if (nextIndex == -1) {
+ // 尝试向后查找连接
+ val firstStart = loop.first().start
+ val prevIndex = remaining.indexOfFirst { it.end == firstStart }
+
+ if (prevIndex != -1) {
+ loop.add(0, remaining.removeAt(prevIndex))
+ } else {
+ break // 无法继续连接
+ }
+ } else {
+ loop.add(remaining.removeAt(nextIndex))
+ }
+
+ // 检查是否形成闭环
+ if (loop.last().end == loop.first().start) {
+ return loop
+ }
+ }
+
+ // 如果没有形成闭环,返回空列表(或者可以根据需求返回部分环)
+ remaining.addAll(loop) // 将线段放回剩余列表
+ return emptyList()
+}
+
+fun Vector3D.rotateAroundZ(angle: Angle): Vector3D {
+ val cosAngle = cos(angle.radians)
+ val sinAngle = sin(angle.radians)
+
+ return Vector3D(
+ x = x * cosAngle - y * sinAngle,
+ y = x * sinAngle + y * cosAngle,
+ z = z
+ )
+}
+
+fun coordinateGenerate(width: Int, height: Int): List {
+ val minX = 0.0
+ val maxX = width.toDouble()
+ val minY = 0.0
+ val maxY = height.toDouble()
+ val minZ = -20.0
+ val maxZ = 20.0
+ val x: () -> Double = { Random.nextDouble(minX, maxX) }
+ val y: () -> Double = { Random.nextDouble(minY, maxY) }
+ val z: () -> Double = { Random.nextDouble(minZ, maxZ) }
+ val dPoints = (0..60).map {
+ Vector3D(x(), y(), z())
+ }
+ return dPoints
+}
+
+fun coordinateGenerate1(): List {
+ val center = Vector3D(0.0, 0.0, 0.0)
+ val direction = Vector3D(0.0, 1.0, -1.0)
+ return (0..360).step(36).map> { degrees: Int ->
+ val newDirection = direction.rotateAroundZ(angle = degrees.degrees)
+ (0..5).map {
+ center + newDirection * it * 100
+ }
+ }.flatten()
+}
+
+
+fun Vector3D.niceStr(): String {
+ return "[$x, $y, $z]".format(this)
+}
+
+fun List.niceStr(): String {
+ return joinToString(", ", "[", "]") {
+ it.niceStr()
+ }
+}
diff --git a/desktop/src/jvmDemo/kotlin/DemoDelaunay3D.kt b/desktop/src/jvmDemo/kotlin/DemoDelaunay3D.kt
new file mode 100644
index 00000000..c1652b51
--- /dev/null
+++ b/desktop/src/jvmDemo/kotlin/DemoDelaunay3D.kt
@@ -0,0 +1,271 @@
+import org.openrndr.KEY_ARROW_DOWN
+import org.openrndr.KEY_ARROW_UP
+import org.openrndr.WindowMultisample
+import org.openrndr.application
+import org.openrndr.color.ColorRGBa
+import org.openrndr.draw.DrawPrimitive
+import org.openrndr.draw.TextSettingMode
+import org.openrndr.draw.loadFont
+import org.openrndr.draw.shadeStyle
+import org.openrndr.extra.camera.Orbital
+import org.openrndr.extra.marchingsquares.findContours
+import org.openrndr.extra.noise.gradientPerturbFractal
+import org.openrndr.extra.objloader.loadOBJasVertexBuffer
+import org.openrndr.extra.textwriter.writer
+import org.openrndr.extra.triangulation.DelaunayTriangulation
+import org.openrndr.math.Vector2
+import org.openrndr.math.Vector3
+import org.openrndr.shape.Path3D
+import org.openrndr.shape.Segment3D
+import org.openrndr.shape.ShapeContour
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+fun main() = application {
+ configure {
+ width = 720
+ height = 720
+ title = "Delaunator"
+ multisample = WindowMultisample.SampleCount(8)
+
+ }
+ program {
+ /*val points3D = (0 until height).step(36).map { y ->
+ (0 until width).step(36).map { x ->
+ gradientPerturbFractal(
+ 300,
+ frequency = 0.8,
+ position = Vector3(x.toDouble(), y.toDouble(), seconds)
+ )
+ }
+ }.flatten().map {
+ it.copy(x = it.x - width / 2, y = it.y - height / 2, z = it.z * 100)
+ }*/
+ /*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
+ width = width,
+ height = height,
+ volcanoCount = 3
+ )*/
+ val points3D = coordinateGenerate(width, height).map {
+ it.copy(x = it.x - width / 2, y = it.y - height / 2)
+ }
+ val zs = points3D.map { it.z }
+ println("zs = ${zs}")
+ val associate: MutableMap = points3D.associate {
+ Vector2(it.x, it.y) to it.z
+ }.toMutableMap()
+ val delaunay = DelaunayTriangulation(associate.map { it.key })
+
+ //println(points3D.niceStr())
+ //extend(Camera2D())
+ val cam = Orbital()
+ extend(cam) {
+ eye = Vector3(x = 100.0, y = 100.0, z = 0.0)
+ lookAt = Vector3(x = 1.6, y = -1.9, z = 1.2)
+ }
+
+ println("draw")
+ var targetHeight: Double = zs.average()
+ var logEnabled = true
+ var useInterpolation = false
+ var sampleLinear = false
+ keyboard.keyDown.listen {
+ logEnabled = true
+ println(it)
+ when (it.key) {
+ KEY_ARROW_UP -> targetHeight++
+ KEY_ARROW_DOWN -> targetHeight--
+ 73 -> useInterpolation = !useInterpolation
+ 83 -> sampleLinear = !sampleLinear
+ }
+ }
+ val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj")
+
+ extend {
+ val triangles = delaunay.triangles()
+ val segments = mutableListOf()
+ drawer.clear(ColorRGBa.BLACK)
+ val indexDiff = (frameCount / 1000) % triangles.size
+ drawer.shadeStyle = shadeStyle {
+ fragmentTransform = """
+ x_fill.rgb = normalize(v_viewNormal) * 0.5 + vec3(0.5);
+ """.trimIndent()
+ }
+
+ drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
+ for ((i, triangle) in triangles.withIndex()) {
+ val segment2DS = triangle.contour.segments.filter {
+ val startZ = associate[it.start]!!
+ val endZ = associate[it.end]!!
+ if (startZ < endZ) {
+ targetHeight in startZ..endZ
+ } else {
+ targetHeight in endZ..startZ
+ }
+ }
+
+ if (segment2DS.size == 2) {
+ val vector2s = segment2DS.map {
+ val startZ = associate[it.start]!!
+ val endZ = associate[it.end]!!
+ val start = Vector3(it.start.x, it.start.y, startZ)
+ val end = Vector3(it.end.x, it.end.y, endZ)
+ if (startZ < endZ) {
+ start to end
+ } else {
+ end to start
+ }
+ }.map { (start, end) ->
+ val segment3D = Segment3D(start, end)
+ val vector3 =
+ segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
+ vector3
+ }.onEach {
+ associate[it.xy] = it.z
+ }
+ val element = Segment3D(vector2s[0], vector2s[1])
+ segments.add(element)
+ }
+ drawer.strokeWeight = 20.0
+ drawer.stroke = ColorRGBa.PINK
+ val segment3DS = triangle.contour.segments.map {
+ val startZ = associate[it.start]!!
+ val endZ = associate[it.end]!!
+ Segment3D(it.start.vector3(z = startZ), it.end.vector3(z = endZ))
+ }
+
+ //drawer.contour(triangle.contour)
+ drawer.path(Path3D.fromSegments(segment3DS, closed = true))
+ }
+
+ val sorted = connectAllSegments(segments)
+
+ drawer.stroke = ColorRGBa.WHITE
+ drawer.strokeWeight = 2.0
+ if (logEnabled) {
+ segments.forEach {
+ println("${it.start} -> ${it.end}")
+ }
+ println("=====")
+ }
+
+ sorted.forEach {
+ it.forEach {
+ if (logEnabled) println("${it.start} -> ${it.end}")
+ drawer.lineSegment(it.start, it.end)
+ drawer.fill = ColorRGBa.WHITE
+ }
+ if (logEnabled) println("=")
+ drawer.fill = ColorRGBa.YELLOW
+ // if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
+ }
+ /*for (y in 0 until (area.height / cellSize).toInt()) {
+ for (x in 0 until (area.width / cellSize).toInt()) {
+ values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
+ }
+ }*/
+ val contours = findContours(
+ f = {
+ val triangle = triangles.firstOrNull { triangle ->
+ isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
+ }
+ triangle ?: return@findContours 0.0
+ val interpolate = interpolateHeight(
+ point = it,
+ triangle = listOf(
+ triangle.x1,
+ triangle.x2,
+ triangle.x3,
+ ).map {
+ Vector3(it.x, it.y, associate[it]!!)
+ }
+ )
+ interpolate.z - targetHeight
+ },
+ area = drawer.bounds.movedTo(Vector2(-width / 2.0, -height / 2.0)),
+ cellSize = 4.0,
+ useInterpolation = useInterpolation
+ )
+ if (logEnabled) println("useInterpolation = $useInterpolation")
+ drawer.stroke = null
+ contours.map {
+ it.segments.map {
+ Segment3D(
+ it.start.vector3(),
+ it.end.vector3()
+ )
+ }
+ }.forEach {
+ drawer.fill = ColorRGBa.GREEN.opacify(0.1)
+ drawer.path(Path3D.fromSegments(it, closed = true))
+ }
+
+ if (false) writer {
+ drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
+ drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
+ text(targetHeight.toString())
+ }
+ logEnabled = false
+ }
+ }
+}
+
+data class Triangle3D(
+ val x1: Vector3,
+ val x2: Vector3,
+ val x3: Vector3,
+) {
+ fun toList(): List = listOf(x1, x2, x3)
+}
+
+fun connectAllSegments(segments: List): List> {
+ val remaining = segments.toMutableList()
+ val allPaths = mutableListOf>()
+
+ while (remaining.isNotEmpty()) {
+ val path = mutableListOf()
+
+ // 开始新路径
+ path.add(remaining.removeAt(0))
+
+ var changed: Boolean
+ do {
+ changed = false
+
+ // 向前扩展
+ val lastEnd = path.last().end
+ val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
+ if (forwardSegment != null) {
+ val connectedSegment = if (forwardSegment.start == lastEnd) {
+ forwardSegment // 正向
+ } else {
+ Segment3D(forwardSegment.end, forwardSegment.start) // 反向
+ }
+ path.add(connectedSegment)
+ remaining.remove(forwardSegment)
+ changed = true
+ }
+
+ // 向后扩展
+ val firstStart = path.first().start
+ val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
+ if (backwardSegment != null) {
+ val connectedSegment = if (backwardSegment.end == firstStart) {
+ backwardSegment // 正向
+ } else {
+ Segment3D(backwardSegment.end, backwardSegment.start) // 反向
+ }
+ path.add(0, connectedSegment)
+ remaining.remove(backwardSegment)
+ changed = true
+ }
+
+ } while (changed && remaining.isNotEmpty())
+
+ allPaths.add(path)
+ }
+
+ return allPaths
+}
diff --git a/desktop/src/jvmDemo/kotlin/FindContours.kt b/desktop/src/jvmDemo/kotlin/FindContours.kt
new file mode 100644
index 00000000..34df6b20
--- /dev/null
+++ b/desktop/src/jvmDemo/kotlin/FindContours.kt
@@ -0,0 +1,94 @@
+import org.openrndr.application
+import org.openrndr.color.ColorRGBa
+import org.openrndr.extra.camera.Camera2D
+import org.openrndr.extra.marchingsquares.findContours
+import org.openrndr.math.Vector2
+import org.openrndr.math.Vector3
+
+/**
+ * A simple demonstration of using the `findContours` method provided by `orx-marching-squares`.
+ *
+ * `findContours` lets one generate contours by providing a mathematical function to be
+ * sampled within the provided area and with the given cell size. Contours are generated
+ * between the areas in which the function returns positive and negative values.
+ *
+ * In this example, the `f` function returns the distance of a point to the center of the window minus 200.0.
+ * Therefore, sampled locations which are less than 200 pixels away from the center return
+ * negative values and all others return positive values, effectively generating a circle of radius 200.0.
+ *
+ * Try increasing the cell size to see how the precision of the circle reduces.
+ *
+ * The circular contour created in this program has over 90 segments. The number of segments depends on the cell
+ * size, and the resulting radius.
+ */
+fun main() = application {
+ configure {
+ width = 720
+ height = 720
+ }
+ program {
+ extend(Camera2D())
+ var showLog = true
+ val target = Vector2(0.0, 0.0)
+ val points3D = (0..10).map { x ->
+ (0..10).map { y ->
+ Vector3(x.toDouble(), y.toDouble(), x * y * 1.0)
+ }
+ }
+
+ extend {
+ drawer.clear(ColorRGBa.BLACK)
+ drawer.stroke = ColorRGBa.PINK
+ fun f3(v: Vector2): Double {
+ val distance = drawer.bounds.center.distanceTo(v)
+ return when (distance) {
+ in 0.0..<100.0 -> -3.0
+ in 100.0..<200.0 -> 1.0
+ in 200.0..300.0 -> -1.0
+ else -> distance
+ }
+ }
+
+ fun f(v: Vector2): Double {
+ val distanceTo = v.distanceTo(target)
+ return (distanceTo - 100.0).also {
+ if (showLog) println(
+ buildString {
+ appendLine("${v} distanceTo ${target} = ${distanceTo}")
+ appendLine("distanceTo - 100.0 = ${distanceTo - 100.0}")
+ }
+ )
+ }
+ }
+
+ val points = mutableListOf()
+
+ fun f1(v: Vector2): Double {
+ val result = if (v.x == v.y * 2 || v.x * 2 == v.y) {
+ points.add(v)
+ -1.0
+ } else 0.0
+ return result.also {
+ if (showLog) {
+ println("$v -> $result")
+ }
+ }
+ }
+
+ val contours = findContours(::f3, drawer.bounds, 4.0)
+ drawer.fill = null
+ drawer.contours(contours)
+
+ if (showLog) {
+ println(
+ buildString {
+ for ((index, contour) in contours.withIndex()) {
+ appendLine("index = ${index}, $contour")
+ }
+ }
+ )
+ }
+ showLog = false
+ }
+ }
+}
diff --git a/desktop/src/jvmDemo/kotlin/HeightmapVolcanoGenerator.kt b/desktop/src/jvmDemo/kotlin/HeightmapVolcanoGenerator.kt
new file mode 100644
index 00000000..eb6a923a
--- /dev/null
+++ b/desktop/src/jvmDemo/kotlin/HeightmapVolcanoGenerator.kt
@@ -0,0 +1,373 @@
+import com.icegps.math.geometry.Vector3D
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.cos
+import kotlin.math.exp
+import kotlin.math.floor
+import kotlin.math.max
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+object HeightmapVolcanoGenerator {
+
+ // 基础火山高度图
+ fun generateVolcanoHeightmap(
+ width: Int = 100,
+ height: Int = 100,
+ centerX: Double = 50.0,
+ centerY: Double = 50.0,
+ maxHeight: Double = 60.0,
+ craterRadius: Double = 8.0,
+ volcanoRadius: Double = 30.0
+ ): List {
+ val points = mutableListOf()
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ // 计算到火山中心的距离
+ val dx = x - centerX
+ val dy = y - centerY
+ val distance = sqrt(dx * dx + dy * dy)
+
+ // 计算基础火山高度
+ var z = calculateVolcanoHeight(distance, craterRadius, volcanoRadius, maxHeight)
+
+ // 添加噪声细节
+ val noise = perlinNoise(x * 0.1, y * 0.1, 0.1) * 3.0
+ z = max(0.0, z + noise)
+
+ points.add(Vector3D(x.toDouble(), y.toDouble(), z))
+ }
+ }
+
+ return points
+ }
+
+ // 复合火山群高度图
+ fun generateVolcanoClusterHeightmap(
+ width: Int = 150,
+ height: Int = 150,
+ volcanoCount: Int = 3
+ ): List {
+ val points = mutableListOf()
+ val volcanoes = generateRandomVolcanoPositions(volcanoCount, width, height)
+
+ for (x in (0 until width).step(25)) {
+ for (y in (0 until height).step(25)) {
+ var totalZ = 0.0
+
+ // 叠加所有火山的影响
+ for (volcano in volcanoes) {
+ val dx = x - volcano.x
+ val dy = y - volcano.y
+ val distance = sqrt(dx * dx + dy * dy)
+
+ if (distance <= volcano.radius) {
+ val volcanoHeight = calculateVolcanoHeight(
+ distance,
+ volcano.craterRadius,
+ volcano.radius,
+ volcano.maxHeight
+ )
+ totalZ += volcanoHeight
+ }
+ }
+
+ // 基础地形
+ val baseNoise = perlinNoise(x * 0.02, y * 0.02, 0.05) * 5.0
+ val detailNoise = perlinNoise(x * 0.1, y * 0.1, 0.2) * 2.0
+
+ points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ + baseNoise + detailNoise))
+ }
+ }
+
+ return points
+ }
+
+ // 带熔岩流的火山高度图
+ fun generateVolcanoWithLavaHeightmap(
+ width: Int = 100,
+ height: Int = 100
+ ): List {
+ val points = mutableListOf()
+ val centerX = width / 2.0
+ val centerY = height / 2.0
+
+ // 生成熔岩流路径
+ val lavaFlows = generateLavaFlowPaths(centerX, centerY, 3)
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val dx = x - centerX
+ val dy = y - centerY
+ val distance = sqrt(dx * dx + dy * dy)
+
+ // 基础火山高度
+ var z = calculateVolcanoHeight(distance, 10.0, 35.0, 70.0)
+
+ // 添加熔岩流
+ z += calculateLavaFlowEffect(x.toDouble(), y.toDouble(), lavaFlows)
+
+ // 侵蚀效果
+ z += calculateErosionEffect(x.toDouble(), y.toDouble(), distance, z)
+
+ points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
+ }
+ }
+
+ return points
+ }
+
+ // 破火山口高度图
+ fun generateCalderaHeightmap(
+ width: Int = 100,
+ height: Int = 100
+ ): List {
+ val points = mutableListOf()
+ val centerX = width / 2.0
+ val centerY = height / 2.0
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val dx = x - centerX
+ val dy = y - centerY
+ val distance = sqrt(dx * dx + dy * dy)
+
+ var z = calculateCalderaHeight(distance, 15.0, 45.0, 50.0)
+
+ // 内部平坦区域细节
+ if (distance < 20) {
+ z += perlinNoise(x * 0.2, y * 0.2, 0.3) * 1.5
+ }
+
+ points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
+ }
+ }
+
+ return points
+ }
+
+ // 线性火山链高度图
+ fun generateVolcanoChainHeightmap(
+ width: Int = 200,
+ height: Int = 100
+ ): List {
+ val points = mutableListOf()
+
+ // 在一条线上生成多个火山
+ val chainCenters = listOf(
+ Vector3D(30.0, 50.0, 0.0),
+ Vector3D(70.0, 50.0, 0.0),
+ Vector3D(110.0, 50.0, 0.0),
+ Vector3D(150.0, 50.0, 0.0),
+ Vector3D(170.0, 50.0, 0.0)
+ )
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ var totalZ = 0.0
+
+ for (center in chainCenters) {
+ val dx = x - center.x
+ val dy = y - center.y
+ val distance = sqrt(dx * dx + dy * dy)
+
+ if (distance <= 25.0) {
+ val volcanoZ = calculateVolcanoHeight(distance, 6.0, 25.0, 40.0)
+ totalZ += volcanoZ
+ }
+ }
+
+ // 添加基底地形,模拟山脉链
+ val baseRidge = calculateMountainRidge(x.toDouble(), y.toDouble(), width, height)
+ totalZ += baseRidge
+
+ points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ))
+ }
+ }
+
+ return points
+ }
+
+ // 辅助函数
+ private data class VolcanoInfo(
+ val x: Double,
+ val y: Double,
+ val radius: Double,
+ val craterRadius: Double,
+ val maxHeight: Double
+ )
+
+ private data class LavaFlowInfo(
+ val startX: Double,
+ val startY: Double,
+ val angle: Double, // 弧度
+ val length: Double,
+ val width: Double,
+ val intensity: Double
+ )
+
+ private fun calculateVolcanoHeight(
+ distance: Double,
+ craterRadius: Double,
+ volcanoRadius: Double,
+ maxHeight: Double
+ ): Double {
+ return when {
+ distance <= craterRadius -> {
+ // 火山口 - 中心凹陷
+ val craterDepth = maxHeight * 0.4
+ craterDepth * (1.0 - distance / craterRadius)
+ }
+
+ distance <= volcanoRadius -> {
+ // 火山锥
+ val slopeDistance = distance - craterRadius
+ val maxSlopeDistance = volcanoRadius - craterRadius
+ val normalized = slopeDistance / maxSlopeDistance
+ maxHeight * (1.0 - normalized * normalized)
+ }
+
+ else -> 0.0
+ }
+ }
+
+ private fun calculateCalderaHeight(
+ distance: Double,
+ innerRadius: Double,
+ outerRadius: Double,
+ rimHeight: Double
+ ): Double {
+ return when {
+ distance <= innerRadius -> {
+ // 平坦的破火山口底部
+ rimHeight * 0.2
+ }
+
+ distance <= outerRadius -> {
+ // 陡峭的边缘
+ val rimDistance = distance - innerRadius
+ val rimWidth = outerRadius - innerRadius
+ val normalized = rimDistance / rimWidth
+ rimHeight * (1.0 - (1.0 - normalized) * (1.0 - normalized))
+ }
+
+ else -> {
+ // 外部平缓斜坡
+ val externalDistance = distance - outerRadius
+ rimHeight * exp(-externalDistance * 0.08)
+ }
+ }
+ }
+
+ private fun calculateLavaFlowEffect(x: Double, y: Double, lavaFlows: List): Double {
+ var effect = 0.0
+
+ for (flow in lavaFlows) {
+ val dx = x - flow.startX
+ val dy = y - flow.startY
+
+ // 计算到熔岩流中心线的距离
+ val flowDirX = cos(flow.angle)
+ val flowDirY = sin(flow.angle)
+
+ val projection = dx * flowDirX + dy * flowDirY
+
+ if (projection in 0.0..flow.length) {
+ val perpendicularX = dx - projection * flowDirX
+ val perpendicularY = dy - projection * flowDirY
+ val perpendicularDist = sqrt(perpendicularX * perpendicularX + perpendicularY * perpendicularY)
+
+ if (perpendicularDist <= flow.width) {
+ val widthFactor = 1.0 - (perpendicularDist / flow.width)
+ val lengthFactor = 1.0 - (projection / flow.length)
+ effect += flow.intensity * widthFactor * lengthFactor
+ }
+ }
+ }
+
+ return effect
+ }
+
+ private fun calculateErosionEffect(x: Double, y: Double, distance: Double, height: Double): Double {
+ // 基于坡度的侵蚀
+ val slopeNoise = perlinNoise(x * 0.15, y * 0.15, 0.1) * 2.0
+ // 基于距离的侵蚀
+ val distanceErosion = if (distance > 25) perlinNoise(x * 0.08, y * 0.08, 0.05) * 1.5 else 0.0
+ return slopeNoise + distanceErosion
+ }
+
+ private fun calculateMountainRidge(x: Double, y: Double, width: Int, height: Int): Double {
+ // 创建山脉基底
+ val ridgeCenter = height / 2.0
+ val distanceToRidge = abs(y - ridgeCenter)
+ val ridgeWidth = height * 0.3
+
+ if (distanceToRidge <= ridgeWidth) {
+ val ridgeFactor = 1.0 - (distanceToRidge / ridgeWidth)
+ return ridgeFactor * 15.0 * perlinNoise(x * 0.01, y * 0.01, 0.02)
+ }
+ return 0.0
+ }
+
+ private fun generateRandomVolcanoPositions(count: Int, width: Int, height: Int): List {
+ return List(count) {
+ VolcanoInfo(
+ x = (width * 0.2 + random() * width * 0.6),
+ y = (height * 0.2 + random() * height * 0.6),
+ radius = 20.0 + random() * 20.0,
+ craterRadius = 5.0 + random() * 7.0,
+ maxHeight = 25.0 + random() * 35.0
+ )
+ }
+ }
+
+ private fun generateLavaFlowPaths(centerX: Double, centerY: Double, count: Int): List {
+ return List(count) {
+ LavaFlowInfo(
+ startX = centerX,
+ startY = centerY,
+ angle = random() * 2 * PI,
+ length = 20.0 + random() * 15.0,
+ width = 2.0 + random() * 3.0,
+ intensity = 5.0 + random() * 8.0
+ )
+ }
+ }
+
+ private fun perlinNoise(x: Double, y: Double, frequency: Double): Double {
+ // 简化的柏林噪声实现
+ val x0 = floor(x * frequency)
+ val y0 = floor(y * frequency)
+ val x1 = x0 + 1
+ val y1 = y0 + 1
+
+ fun grad(ix: Int, iy: Int): Double {
+ val random = sin(ix * 12.9898 + iy * 78.233) * 43758.5453
+ return (random % 1.0) * 2 - 1
+ }
+
+ fun interpolate(a: Double, b: Double, w: Double): Double {
+ return a + (b - a) * (w * w * (3 - 2 * w))
+ }
+
+ val g00 = grad(x0.toInt(), y0.toInt())
+ val g10 = grad(x1.toInt(), y0.toInt())
+ val g01 = grad(x0.toInt(), y1.toInt())
+ val g11 = grad(x1.toInt(), y1.toInt())
+
+ val tx = x * frequency - x0
+ val ty = y * frequency - y0
+
+ val n0 = interpolate(g00, g10, tx)
+ val n1 = interpolate(g01, g11, tx)
+
+ return interpolate(n0, n1, ty)
+ }
+
+ private fun random(): Double = Math.random()
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 211787a3..c066fcca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,13 @@ kotlin.mpp.import.legacyTestSourceSetDetection=true
# Enable Dokka 2.0.0
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
-org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
\ No newline at end of file
+org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5523d419..68eff4f4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -25,6 +25,18 @@ mockk = "1.14.2"
processing = "4.4.10"
nmcp = "1.2.0"
okhttp = "5.2.1"
+agp = "8.13.1"
+junit = "4.13.2"
+coreKtx = "1.17.0"
+junitVersion = "1.3.0"
+espressoCoreVersion = "3.7.0"
+appcompat = "1.7.1"
+material = "1.13.0"
+activity = "1.12.0"
+constraintlayout = "2.2.1"
+lifecycleRuntimeKtx = "2.10.0"
+kotlinx-serialization = "1.9.0"
+mapbox = "11.16.6"
[libraries]
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
@@ -57,10 +69,32 @@ antlr-core = { group = "org.antlr", name = "antlr4", version.ref = "antlr" }
antlr-runtime = { group = "org.antlr", name = "antlr4-runtime", version.ref = "antlr" }
antlr-kotlin-runtime = { group = "com.strumenta", name = "antlr-kotlin-runtime", version.ref = "antlrKotlin" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCoreVersion" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+#ktor
+ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
+ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
+ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
+ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
+ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
+
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
+mapbox-maps = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapbox" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
nebula-release = { id = "nebula.release", version.ref = "nebulaRelease" }
kotest-multiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest" }
antlr-kotlin = { id = "com.strumenta.antlr-kotlin", version.ref = "antlrKotlin" }
-nmcp = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }
\ No newline at end of file
+nmcp = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
\ No newline at end of file
diff --git a/icegps-common/.gitignore b/icegps-common/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/icegps-common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/icegps-common/build.gradle.kts b/icegps-common/build.gradle.kts
new file mode 100644
index 00000000..df9b7422
--- /dev/null
+++ b/icegps-common/build.gradle.kts
@@ -0,0 +1,44 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.icegps.common"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ minSdk = 28
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+kotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_17
+}
+
+dependencies {
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/icegps-common/consumer-rules.pro b/icegps-common/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/icegps-common/proguard-rules.pro b/icegps-common/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/icegps-common/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/icegps-common/src/androidTest/java/com/icegps/common/ExampleInstrumentedTest.kt b/icegps-common/src/androidTest/java/com/icegps/common/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..dbc8dd56
--- /dev/null
+++ b/icegps-common/src/androidTest/java/com/icegps/common/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.icegps.common
+
+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.common.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/icegps-common/src/main/AndroidManifest.xml b/icegps-common/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/icegps-common/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/icegps-common/src/main/java/com/icegps/common/helper/BlhToEnu.kt b/icegps-common/src/main/java/com/icegps/common/helper/BlhToEnu.kt
new file mode 100644
index 00000000..5e1b0c8b
--- /dev/null
+++ b/icegps-common/src/main/java/com/icegps/common/helper/BlhToEnu.kt
@@ -0,0 +1,274 @@
+package com.icegps.common.helper
+
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.pow
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+/**
+ * BLH -> ENU
+ *
+ * @author lm
+ * @date 2025/3/12
+ * @link https://gist.github.com/komasaru/6ce0634475923ddac597f868288c54e9
+ */
+class BlhToEnu {
+
+ companion object {
+ private const val PI_180 = Math.PI / 180.0
+
+ // WGS84 坐标参数
+ private const val A = 6378137.0 // a(地球椭球体长半径(赤道面平均半径))
+ private const val ONE_F = 298.257223563 // 1 / f(地球椭球体扁平率 = (a - b) / a)
+ private val B = A * (1.0 - 1.0 / ONE_F) // b(地球椭球体短半径)
+ private val E2 = (1.0 / ONE_F) * (2 - (1.0 / ONE_F))
+
+ // e^2 = 2 * f - f * f = (a^2 - b^2) / a^2
+ private val ED2 = E2 * A * A / (B * B) // e'^2 = (a^2 - b^2) / b^2
+ }
+
+ private var originLat: Double = 0.0
+ private var originLon: Double = 0.0
+ private var originHeight: Double = 0.0
+ private var isOriginSet: Boolean = false
+
+ fun getOriginLat(): Double = originLat
+
+ fun getOriginLon(): Double = originLon
+
+ fun getOriginHeight(): Double = originHeight
+
+ fun resetEnuBenchmarkPoint() {
+ isOriginSet = false
+ }
+
+ fun wgs84ToEnu(lat: Double, lon: Double, height: Double = 0.0): DoubleArray {
+ if (!isOriginSet) {
+ originLat = lat
+ originLon = lon
+ originHeight = height
+ isOriginSet = true
+ return doubleArrayOf(0.0, 0.0, 0.0)
+ }
+
+ val enu = blh2enu(originLat, originLon, originHeight, lat, lon, height)
+// var az = atan2(enu[0], enu[1]) * 180.0 / Math.PI
+// if (az < 0.0) {
+// az += 360.0
+// }
+// val el = atan2(
+// enu[2],
+// sqrt(enu[0] * enu[0] + enu[1] * enu[1])
+// ) * 180.0 / Math.PI
+// val dst = sqrt(enu.sumOf { it * it })
+
+// println("--->")
+// println(
+// """
+// ENU: E = ${enu[0].format(3)}m
+// N = ${enu[1].format(3)}m
+// U = ${enu[2].format(3)}m
+// 方位角 = ${az.format(3)}°
+// 仰角 = ${el.format(3)}°
+// 距离 = ${dst.format(3)}m
+// """.trimIndent()
+// )
+
+ return enu
+ }
+
+ fun enuToWgs84(e: Double, n: Double, u: Double): DoubleArray {
+ if (!isOriginSet) {
+ return doubleArrayOf(0.0, 0.0, 0.0)
+ }
+
+ val blh = enu2blh(originLat, originLon, originHeight, e, n, u)
+
+// println("--->")
+// println(
+// """
+// BLH: Beta = ${blh[0].format(8)}°
+// Lambda = ${blh[1].format(8)}°
+// Height = ${blh[2].format(3)}m
+// """.trimIndent()
+// )
+
+ return blh
+ }
+
+ private fun Double.format(digits: Int) = "%.${digits}f".format(this)
+
+ /**
+ * BLH -> ENU 转换(East, North, Up)
+ *
+ * @param bO 原点 Beta(纬度)
+ * @param lO 原点 Lambda(经度)
+ * @param hO 原点 Height(高度)
+ * @param b 目标点 Beta(纬度)
+ * @param l 目标点 Lambda(经度)
+ * @param h 目标点 Height(高度)
+ * @return ENU 坐标 [e, n, u]
+ */
+ private fun blh2enu(bO: Double, lO: Double, hO: Double, b: Double, l: Double, h: Double): DoubleArray {
+ val (xO, yO, zO) = blh2ecef(bO, lO, hO)
+ val (x, y, z) = blh2ecef(b, l, h)
+ val mat0 = matZ(90.0)
+ val mat1 = matY(90.0 - bO)
+ val mat2 = matZ(lO)
+ val mat = mulMat(mulMat(mat0, mat1), mat2)
+ return rotate(mat, doubleArrayOf(x - xO, y - yO, z - zO))
+ }
+
+ /**
+ * BLH -> ECEF 转换
+ *
+ * @param lat 纬度
+ * @param lon 经度
+ * @param height 高度
+ * @return ECEF 坐标 [x, y, z]
+ */
+ private fun blh2ecef(lat: Double, lon: Double, height: Double): DoubleArray {
+ val n = { x: Double -> A / sqrt(1.0 - E2 * sin(x * PI_180).pow(2)) }
+ val x = (n(lat) + height) * cos(lat * PI_180) * cos(lon * PI_180)
+ val y = (n(lat) + height) * cos(lat * PI_180) * sin(lon * PI_180)
+ val z = (n(lat) * (1.0 - E2) + height) * sin(lat * PI_180)
+ return doubleArrayOf(x, y, z)
+ }
+
+ /**
+ * ENU -> BLH 转换
+ *
+ * @param e East 坐标
+ * @param n North 坐标
+ * @param u Up 坐标
+ * @return WGS84 坐标 [纬度, 经度, 高度]
+ */
+ private fun enu2blh(bO: Double, lO: Double, hO: Double, e: Double, n: Double, u: Double): DoubleArray {
+ val mat0 = matZ(-lO)
+ val mat1 = matY(-(90.0 - bO))
+ val mat2 = matZ(-90.0)
+ val mat = mulMat(mulMat(mat0, mat1), mat2)
+
+ val enu = doubleArrayOf(e, n, u)
+ val xyz = rotate(mat, enu)
+
+ val (xO, yO, zO) = blh2ecef(bO, lO, hO)
+ val x = xyz[0] + xO
+ val y = xyz[1] + yO
+ val z = xyz[2] + zO
+
+ return ecef2blh(x, y, z)
+ }
+
+ /**
+ * ECEF -> BLH 转换
+ *
+ * @param x ECEF X 坐标
+ * @param y ECEF Y 坐标
+ * @param z ECEF Z 坐标
+ * @return WGS84 坐标 [纬度, 经度, 高度]
+ */
+ private fun ecef2blh(x: Double, y: Double, z: Double): DoubleArray {
+ val p = sqrt(x * x + y * y)
+ val theta = atan2(z * A, p * B)
+ val sinTheta = sin(theta)
+ val cosTheta = cos(theta)
+
+ val lat = atan2(
+ z + ED2 * B * sinTheta.pow(3),
+ p - E2 * A * cosTheta.pow(3)
+ )
+ val lon = atan2(y, x)
+
+ val sinLat = sin(lat)
+ val n = A / sqrt(1.0 - E2 * sinLat * sinLat)
+ val h = p / cos(lat) - n
+
+ return doubleArrayOf(
+ lat * 180.0 / Math.PI,
+ lon * 180.0 / Math.PI,
+ h
+ )
+ }
+
+ /**
+ * 以 x 轴为轴的旋转矩阵
+ *
+ * @param ang 旋转角度(°)
+ * @return 旋转矩阵(3x3)
+ */
+ private fun matX(ang: Double): Array {
+ val a = ang * PI_180
+ val c = cos(a)
+ val s = sin(a)
+ return arrayOf(
+ doubleArrayOf(1.0, 0.0, 0.0),
+ doubleArrayOf(0.0, c, s),
+ doubleArrayOf(0.0, -s, c)
+ )
+ }
+
+ /**
+ * 以 y 轴为轴的旋转矩阵
+ *
+ * @param ang 旋转角度(°)
+ * @return 旋转矩阵(3x3)
+ */
+ private fun matY(ang: Double): Array {
+ val a = ang * PI_180
+ val c = cos(a)
+ val s = sin(a)
+ return arrayOf(
+ doubleArrayOf(c, 0.0, -s),
+ doubleArrayOf(0.0, 1.0, 0.0),
+ doubleArrayOf(s, 0.0, c)
+ )
+ }
+
+ /**
+ * 以 z 轴为轴的旋转矩阵
+ *
+ * @param ang 旋转角度(°)
+ * @return 旋转矩阵(3x3)
+ */
+ private fun matZ(ang: Double): Array {
+ val a = ang * PI_180
+ val c = cos(a)
+ val s = sin(a)
+ return arrayOf(
+ doubleArrayOf(c, s, 0.0),
+ doubleArrayOf(-s, c, 0.0),
+ doubleArrayOf(0.0, 0.0, 1.0)
+ )
+ }
+
+ /**
+ * 两个矩阵(3x3)的乘积
+ *
+ * @param matA 3x3 矩阵
+ * @param matB 3x3 矩阵
+ * @return 3x3 矩阵
+ */
+ private fun mulMat(matA: Array, matB: Array): Array {
+ return Array(3) { k ->
+ DoubleArray(3) { j ->
+ (0..2).sumOf { i -> matA[k][i] * matB[i][j] }
+ }
+ }
+ }
+
+ /**
+ * 点的旋转
+ *
+ * @param mat 3x3 旋转矩阵
+ * @param pt 旋转前坐标 [x, y, z]
+ * @return 旋转后坐标 [x, y, z]
+ */
+ private fun rotate(mat: Array, pt: DoubleArray): DoubleArray {
+ return DoubleArray(3) { j ->
+ (0..2).sumOf { i -> mat[j][i] * pt[i] }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/icegps-common/src/main/java/com/icegps/common/helper/GeoHelper.kt b/icegps-common/src/main/java/com/icegps/common/helper/GeoHelper.kt
new file mode 100644
index 00000000..7fdc7f04
--- /dev/null
+++ b/icegps-common/src/main/java/com/icegps/common/helper/GeoHelper.kt
@@ -0,0 +1,419 @@
+package com.icegps.common.helper
+
+import android.os.Parcel
+import android.os.Parcelable
+import kotlin.math.atan
+import kotlin.math.atan2
+import kotlin.math.cbrt
+import kotlin.math.cos
+import kotlin.math.exp
+import kotlin.math.ln
+import kotlin.math.sin
+import kotlin.math.sqrt
+import kotlin.math.tan
+
+/**
+ * WGS84、EPSG3857、ENU 的坐标转换工具类
+ *
+ * @author lm
+ * @date 2024/8/2
+ */
+class GeoHelper private constructor() {
+
+ companion object {
+ private var sharedInstance: GeoHelper? = null
+ fun getSharedInstance(): GeoHelper = sharedInstance ?: GeoHelper().also { sharedInstance = it }
+ fun createInstance(): GeoHelper = GeoHelper()
+ }
+
+ // WGS-84 ellipsoid parameters
+ private val RADIUS = 6378137.0 // Major radius
+ private val RADIUS_B = 6356752.314245 // Minor radius
+ private val E = (RADIUS * RADIUS - RADIUS_B * RADIUS_B) / (RADIUS * RADIUS) // Eccentricity
+ private val HALF_SIZE = Math.PI * RADIUS // Half circumference of Earth
+ private val DEG2RAD = Math.PI / 180 // Degrees to radians conversion factor
+ private val RAD2DEG = 180 / Math.PI // Radians to degrees conversion factor
+ private val RE_WGS84 = 6378137.0 // Earth's equatorial radius in WGS84
+ private val FE_WGS84 = 1.0 / 298.257223563 // Flattening of the WGS84 ellipsoid
+
+ private var isFirstPoint = true
+ private var firstPoint = DoubleArray(3)
+ private val bPos = DoubleArray(3)
+ private var bECEF = DoubleArray(3)
+ private val rPos = DoubleArray(3)
+ private var rECEF = DoubleArray(3)
+ private val vECEF = DoubleArray(3)
+
+ private var useBlhToEnu = true
+ private var blhToEnu = BlhToEnu()
+
+ /**
+ * 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标
+ * 如果是第一个点,它将被设置为 ENU 坐标系的基准点
+ *
+ * @param lon 经度(度)
+ * @param lat 纬度(度)
+ * @param hgt 高度(米)
+ * @return 包含 ENU 坐标的 Enu 对象
+ */
+ fun wgs84ToENU(lon: Double, lat: Double, hgt: Double): ENU {
+ if (useBlhToEnu) {
+ val enu = blhToEnu.wgs84ToEnu(lon = lon, lat = lat, height = hgt)
+ return ENU(enu[0], enu[1], enu[2])
+ }
+
+ if (isFirstPoint) setEnuBenchmark(lon, lat, hgt)
+ rPos[0] = lat * DEG2RAD
+ rPos[1] = lon * DEG2RAD
+ rPos[2] = hgt
+
+ rECEF = pos2ecef(rPos)
+ vECEF[0] = rECEF[0] - bECEF[0]
+ vECEF[1] = rECEF[1] - bECEF[1]
+ vECEF[2] = rECEF[2] - bECEF[2]
+ val enuDoubleArray = ecef2enu(bPos, vECEF)
+ return ENU(enuDoubleArray[0], enuDoubleArray[1], enuDoubleArray[2])
+ }
+
+ /**
+ * 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标
+ * 如果是第一个点,它将被设置为 ENU 坐标系的基准点
+ *
+ * @param wgs84 WGS84 坐标对象
+ * @return 包含 ENU 坐标的 Enu 对象
+ */
+ fun wgs84ObjectToENU(wgs84: WGS84): ENU = wgs84ToENU(wgs84.lon, wgs84.lat, wgs84.hgt)
+
+ /**
+ * 是否已设置 ENU 坐标系的基准点
+ */
+ fun isEnuBenchmarkSet(): Boolean = !isFirstPoint
+
+ /**
+ * 设置 ENU 坐标系的基准点
+ *
+ * @param lon 基准点经度(度)
+ * @param lat 基准点纬度(度)
+ * @param hgt 基准点高度(米)
+ */
+ private fun setEnuBenchmark(lon: Double, lat: Double, hgt: Double) {
+ firstPoint = doubleArrayOf(lon, lat, hgt)
+ bPos[0] = lat * DEG2RAD
+ bPos[1] = lon * DEG2RAD
+ bPos[2] = hgt
+ bECEF = pos2ecef(bPos)
+ isFirstPoint = false
+ }
+
+ /**
+ * 获取 ENU 坐标系的基准点
+ *
+ * @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray
+ */
+ fun getEnuBenchmarkPoint(): DoubleArray {
+ if (useBlhToEnu) {
+ return doubleArrayOf(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight())
+ }
+ return firstPoint
+ }
+
+ /**
+ * 获取 ENU 坐标系的基准点
+ *
+ * @return 包含 WGS84 坐标的 WGS84 对象
+ */
+ fun getEnuBenchmarkPointAsWGS84(): WGS84 {
+ if (useBlhToEnu) {
+ return WGS84(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight())
+ }
+ return WGS84(firstPoint[0], firstPoint[1], firstPoint[2])
+ }
+
+ /**
+ * 重置 ENU 基准点
+ * 调用此方法后,下一次 wgs84ToENU 调用将设置新的基准点
+ */
+ fun resetEnuBenchmarkPoint() {
+ if (useBlhToEnu) {
+ blhToEnu.resetEnuBenchmarkPoint()
+ return
+ }
+ isFirstPoint = true
+ }
+
+ /**
+ * 将 ENU (East-North-Up) 坐标转换为 WGS84 坐标
+ *
+ * @param enu 包含 ENU 坐标的 Enu 对象
+ * @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray
+ */
+ fun enuToWGS84(enu: ENU): DoubleArray {
+ if (useBlhToEnu) {
+ val wgs84 = blhToEnu.enuToWgs84(e = enu.x, n = enu.y, u = enu.z)
+ return doubleArrayOf(wgs84[1], wgs84[0], wgs84[2])
+ }
+
+ val enuArray = doubleArrayOf(enu.x, enu.y, enu.z)
+ val enuToEcefMatrix = xyz2enu(bPos)
+ val ecefArray = matmul(charArrayOf('T', 'N'), 3, 1, 3, 1.0, enuToEcefMatrix, enuArray, 0.0)
+ vECEF[0] = bECEF[0] + ecefArray[0]
+ vECEF[1] = bECEF[1] + ecefArray[1]
+ vECEF[2] = bECEF[2] + ecefArray[2]
+ return ecef2pos(vECEF)
+ }
+
+ /**
+ * 将 ENU (East-North-Up) 坐标转换为 WGS84 坐标
+ *
+ * @param enu 包含 ENU 坐标的 Enu 对象
+ * @return 包含 WGS84 坐标的 WGS84 对象
+ */
+ fun enuToWGS84Object(enu: ENU): WGS84 {
+ val wgs84Array = enuToWGS84(enu)
+ return WGS84(wgs84Array[0], wgs84Array[1], wgs84Array[2])
+ }
+
+ /**
+ * 将 WGS84 坐标转换为 EPSG3857 坐标
+ *
+ * @param lon 经度(度)
+ * @param lat 纬度(度)
+ * @return 包含 EPSG3857 坐标的 EPSG3857 对象
+ */
+ fun wgs84ToEPSG3857(lon: Double, lat: Double): EPSG3857 {
+ val x = lon * HALF_SIZE / 180
+ var y = RADIUS * ln(tan(Math.PI * (lat + 90) / 360))
+ y = y.coerceIn(-HALF_SIZE, HALF_SIZE)
+ return EPSG3857(x, y)
+ }
+
+ /**
+ * 将 WGS84 坐标转换为 EPSG3857 坐标
+ *
+ * @param wgs84 WGS84 坐标对象
+ * @return 包含 EPSG3857 坐标的 EPSG3857 对象
+ */
+ fun wgs84ObjectToEPSG3857(wgs84: WGS84): EPSG3857 = wgs84ToEPSG3857(wgs84.lon, wgs84.lat)
+
+ /**
+ * 将 EPSG3857 坐标转换为 WGS84 坐标
+ *
+ * @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象
+ * @return 包含 WGS84 坐标 {经度, 纬度} 的 DoubleArray
+ */
+ fun epsg3857ToWGS84(epsg3857: EPSG3857): DoubleArray {
+ val lon = (epsg3857.x / HALF_SIZE) * 180.0
+ val lat = (2 * atan(exp(epsg3857.y / RADIUS)) - Math.PI / 2) * RAD2DEG
+ return doubleArrayOf(lon, lat)
+ }
+
+ /**
+ * 将 EPSG3857 坐标转换为 WGS84 坐标
+ *
+ * @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象
+ * @return 包含 WGS84 坐标的 WGS84 对象
+ */
+ fun epsg3857ToWGS84Object(epsg3857: EPSG3857): WGS84 {
+ val wgs84Array = epsg3857ToWGS84(epsg3857)
+ return WGS84(wgs84Array[0], wgs84Array[1], 0.0)
+ }
+
+ fun pos2ecef(pos: DoubleArray): DoubleArray {
+ val (lat, lon, hgt) = pos
+ val sinp = sin(lat)
+ val cosp = cos(lat)
+ val sin_l = sin(lon)
+ val cos_l = cos(lon)
+ val e2 = FE_WGS84 * (2.0 - FE_WGS84)
+ val v = RE_WGS84 / sqrt(1.0 - e2 * sinp * sinp)
+
+ return doubleArrayOf(
+ (v + hgt) * cosp * cos_l,
+ (v + hgt) * cosp * sin_l,
+ (v * (1.0 - e2) + hgt) * sinp
+ )
+ }
+
+ fun ecef2enu(pos: DoubleArray, r: DoubleArray): DoubleArray {
+ val E = xyz2enu(pos)
+ return matmul(charArrayOf('N', 'N'), 3, 1, 3, 1.0, E, r, 0.0)
+ }
+
+ fun matmul(
+ tr: CharArray,
+ n: Int,
+ k: Int,
+ m: Int,
+ alpha: Double,
+ A: DoubleArray,
+ B: DoubleArray,
+ beta: Double
+ ): DoubleArray {
+ val f = when {
+ tr[0] == 'N' && tr[1] == 'N' -> 1
+ tr[0] == 'N' && tr[1] == 'T' -> 2
+ tr[0] == 'T' && tr[1] == 'N' -> 3
+ else -> 4
+ }
+ val C = DoubleArray(n * k)
+ for (i in 0 until n) {
+ for (j in 0 until k) {
+ var d = 0.0
+ when (f) {
+ 1 -> for (x in 0 until m) d += A[i + x * n] * B[x + j * m]
+ 2 -> for (x in 0 until m) d += A[i + x * n] * B[j + x * k]
+ 3 -> for (x in 0 until m) d += A[x + i * m] * B[x + j * m]
+ 4 -> for (x in 0 until m) d += A[x + i * m] * B[j + x * k]
+ }
+ C[i + j * n] = alpha * d + beta * C[i + j * n]
+ }
+ }
+ return C
+ }
+
+ fun xyz2enu(pos: DoubleArray): DoubleArray {
+ val (lat, lon) = pos
+ val sinp = sin(lat)
+ val cosp = cos(lat)
+ val sin_l = sin(lon)
+ val cos_l = cos(lon)
+
+ return doubleArrayOf(
+ -sin_l, cos_l, 0.0,
+ -sinp * cos_l, -sinp * sin_l, cosp,
+ cosp * cos_l, cosp * sin_l, sinp
+ )
+ }
+
+ fun ecef2pos(ecef: DoubleArray): DoubleArray {
+ val (x, y, z) = ecef
+ val a = RE_WGS84
+ val b = a * (1 - FE_WGS84)
+ val e2 = (a * a - b * b) / (a * a)
+ val e2p = (a * a - b * b) / (b * b)
+ val r2 = x * x + y * y
+ val r = sqrt(r2)
+ val E2 = a * a - b * b
+ val F = 54 * b * b * z * z
+ val G = r2 + (1 - e2) * z * z - e2 * E2
+ val c = (e2 * e2 * F * r2) / (G * G * G)
+ val s = cbrt(1 + c + sqrt(c * c + 2 * c))
+ val P = F / (3 * (s + 1 / s + 1) * (s + 1 / s + 1) * G * G)
+ val Q = sqrt(1 + 2 * e2 * e2 * P)
+ val r0 = -(P * e2 * r) / (1 + Q) + sqrt(0.5 * a * a * (1 + 1.0 / Q) - P * (1 - e2) * z * z / (Q * (1 + Q)) - 0.5 * P * r2)
+ val U = sqrt((r - e2 * r0) * (r - e2 * r0) + z * z)
+ val V = sqrt((r - e2 * r0) * (r - e2 * r0) + (1 - e2) * z * z)
+ val Z0 = b * b * z / (a * V)
+
+ val lon = atan2(y, x) * RAD2DEG
+ val lat = atan((z + e2p * Z0) / r) * RAD2DEG
+ val hgt = U * (1 - b * b / (a * V))
+ return doubleArrayOf(lon, lat, hgt)
+ }
+
+ data class WGS84(var lon: Double = 0.0, var lat: Double = 0.0, var hgt: Double = 0.0) : Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readDouble(),
+ parcel.readDouble(),
+ parcel.readDouble()
+ )
+
+ constructor(wgs84: DoubleArray) : this(
+ lon = wgs84.getOrElse(0) { 0.0 },
+ lat = wgs84.getOrElse(1) { 0.0 },
+ hgt = wgs84.getOrElse(2) { 0.0 }
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeDouble(lon)
+ parcel.writeDouble(lat)
+ parcel.writeDouble(hgt)
+ }
+
+ override fun describeContents(): Int = 0
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): WGS84 {
+ return WGS84(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
+ override fun toString(): String {
+ return "WGS84(lon=$lon, lat=$lat, hgt=$hgt)"
+ }
+ }
+
+ data class EPSG3857(var x: Double = 0.0, var y: Double = 0.0) : Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readDouble(),
+ parcel.readDouble()
+ )
+
+ constructor(epsG3857: DoubleArray) : this(
+ x = epsG3857.getOrElse(0) { 0.0 },
+ y = epsG3857.getOrElse(1) { 0.0 }
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeDouble(x)
+ parcel.writeDouble(y)
+ }
+
+ override fun describeContents(): Int = 0
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): EPSG3857 {
+ return EPSG3857(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
+ override fun toString(): String {
+ return "EPSG3857(x=$x, y=$y)"
+ }
+ }
+
+ data class ENU(var x: Double = 0.0, var y: Double = 0.0, var z: Double = 0.0) : Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readDouble(),
+ parcel.readDouble(),
+ parcel.readDouble()
+ )
+
+ constructor(enu: DoubleArray) : this(
+ x = enu.getOrElse(0) { 0.0 },
+ y = enu.getOrElse(1) { 0.0 },
+ z = enu.getOrElse(2) { 0.0 }
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeDouble(x)
+ parcel.writeDouble(y)
+ parcel.writeDouble(z)
+ }
+
+ override fun describeContents(): Int = 0
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): ENU {
+ return ENU(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
+ override fun toString(): String {
+ return "ENU(x=$x, y=$y, z=$z)"
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/icegps-common/src/test/java/com/icegps/common/ExampleUnitTest.kt b/icegps-common/src/test/java/com/icegps/common/ExampleUnitTest.kt
new file mode 100644
index 00000000..1a2aaced
--- /dev/null
+++ b/icegps-common/src/test/java/com/icegps/common/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.icegps.common
+
+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)
+ }
+}
\ No newline at end of file
diff --git a/icegps-shared/.gitignore b/icegps-shared/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/icegps-shared/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/icegps-shared/build.gradle.kts b/icegps-shared/build.gradle.kts
new file mode 100644
index 00000000..3324f5fa
--- /dev/null
+++ b/icegps-shared/build.gradle.kts
@@ -0,0 +1,57 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+ namespace = "com.icegps.shared"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ minSdk = 28
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ 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)
+
+ api(libs.kotlinx.serialization.json)
+ api(libs.ktor.client.core)
+ api(libs.ktor.client.cio)
+ api(libs.ktor.serialization.kotlinx.json)
+ api(libs.ktor.client.content.negotiation)
+ api(libs.ktor.client.logging)
+ api(project(":math"))
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/icegps-shared/consumer-rules.pro b/icegps-shared/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/icegps-shared/proguard-rules.pro b/icegps-shared/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/icegps-shared/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/icegps-shared/src/androidTest/java/com/icegps/shared/ExampleInstrumentedTest.kt b/icegps-shared/src/androidTest/java/com/icegps/shared/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..ea37843a
--- /dev/null
+++ b/icegps-shared/src/androidTest/java/com/icegps/shared/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.icegps.shared
+
+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.shared.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/icegps-shared/src/main/AndroidManifest.xml b/icegps-shared/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/icegps-shared/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/icegps-shared/src/main/java/com/icegps/shared/SharedHttpClient.kt b/icegps-shared/src/main/java/com/icegps/shared/SharedHttpClient.kt
new file mode 100644
index 00000000..765421e2
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/SharedHttpClient.kt
@@ -0,0 +1,42 @@
+package com.icegps.shared
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.plugins.logging.SIMPLE
+import io.ktor.http.ContentType
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/20
+ */
+@Suppress("FunctionName")
+fun SharedHttpClient(json: Json): HttpClient {
+ return HttpClient(CIO) {
+ install(ContentNegotiation) {
+ json(
+ json = json,
+ contentType = ContentType.Text.Html
+ )
+ json(
+ json = json,
+ contentType = ContentType.Application.Json
+ )
+ }
+ install(Logging) {
+ this.level = LogLevel.ALL
+ this.logger = Logger.SIMPLE
+ }
+ install(HttpTimeout) {
+ requestTimeoutMillis = 1000 * 60 * 10
+ connectTimeoutMillis = 1000 * 60 * 5
+ socketTimeoutMillis = 1000 * 60 * 10
+ }
+ }
+}
\ No newline at end of file
diff --git a/icegps-shared/src/main/java/com/icegps/shared/SharedJson.kt b/icegps-shared/src/main/java/com/icegps/shared/SharedJson.kt
new file mode 100644
index 00000000..bd969235
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/SharedJson.kt
@@ -0,0 +1,17 @@
+package com.icegps.shared
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/20
+ */
+@Suppress("FunctionName")
+fun SharedJson(): Json {
+ return Json {
+ ignoreUnknownKeys = true
+ serializersModule = SerializersModule {
+ }
+ }
+}
\ No newline at end of file
diff --git a/icegps-shared/src/main/java/com/icegps/shared/api/LookupResponse.kt b/icegps-shared/src/main/java/com/icegps/shared/api/LookupResponse.kt
new file mode 100644
index 00000000..324e6d20
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/api/LookupResponse.kt
@@ -0,0 +1,21 @@
+package com.icegps.shared.api
+
+import com.icegps.shared.model.IGeoPoint
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LookupResponse(
+ @SerialName("results")
+ val results: List
+) {
+ @Serializable
+ data class Result(
+ @SerialName("longitude")
+ override val longitude: Double,
+ @SerialName("latitude")
+ override val latitude: Double,
+ @SerialName("elevation")
+ override val altitude: Double,
+ ) : IGeoPoint
+}
\ No newline at end of file
diff --git a/icegps-shared/src/main/java/com/icegps/shared/api/OpenElevationApi.kt b/icegps-shared/src/main/java/com/icegps/shared/api/OpenElevationApi.kt
new file mode 100644
index 00000000..6ac3f9d3
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/api/OpenElevationApi.kt
@@ -0,0 +1,35 @@
+package com.icegps.shared.api
+
+import com.icegps.shared.model.IGeoPoint
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.client.request.parameter
+import io.ktor.http.appendPathSegments
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/20
+ */
+interface OpenElevationApi {
+ suspend fun lookup(values: List): List
+}
+
+class OpenElevation(
+ private val client: HttpClient
+) : OpenElevationApi {
+ private val baseUrl: String = "https://api.open-elevation.com/api/v1/"
+
+ // curl 'https://api.open-elevation.com/api/v1/lookup?locations=10,10|20,20|41.161758,-8.583933'
+ override suspend fun lookup(values: List): List {
+ val response = client.get(baseUrl) {
+ url {
+ appendPathSegments("lookup")
+ parameter(
+ "locations",
+ values.joinToString("|") { "${it.latitude},${it.longitude}" })
+ }
+ }
+ return response.body().results
+ }
+}
\ No newline at end of file
diff --git a/icegps-shared/src/main/java/com/icegps/shared/ktx/Any.kt b/icegps-shared/src/main/java/com/icegps/shared/ktx/Any.kt
new file mode 100644
index 00000000..9f2d51ac
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/ktx/Any.kt
@@ -0,0 +1,7 @@
+package com.icegps.shared.ktx
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+val Any.TAG: String get() = this::class.java.simpleName
diff --git a/icegps-shared/src/main/java/com/icegps/shared/model/GeoPoint.kt b/icegps-shared/src/main/java/com/icegps/shared/model/GeoPoint.kt
new file mode 100644
index 00000000..383a829d
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/model/GeoPoint.kt
@@ -0,0 +1,11 @@
+package com.icegps.shared.model
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+data class GeoPoint(
+ override val longitude: Double,
+ override val latitude: Double,
+ override val altitude: Double
+) : IGeoPoint
diff --git a/icegps-shared/src/main/java/com/icegps/shared/model/IGeoPoint.kt b/icegps-shared/src/main/java/com/icegps/shared/model/IGeoPoint.kt
new file mode 100644
index 00000000..ee8def6e
--- /dev/null
+++ b/icegps-shared/src/main/java/com/icegps/shared/model/IGeoPoint.kt
@@ -0,0 +1,11 @@
+package com.icegps.shared.model
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/11/22
+ */
+interface IGeoPoint {
+ val longitude: Double
+ val latitude: Double
+ val altitude: Double
+}
\ No newline at end of file
diff --git a/icegps-shared/src/test/java/com/icegps/shared/ExampleUnitTest.kt b/icegps-shared/src/test/java/com/icegps/shared/ExampleUnitTest.kt
new file mode 100644
index 00000000..7dd9b85f
--- /dev/null
+++ b/icegps-shared/src/test/java/com/icegps/shared/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.icegps.shared
+
+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)
+ }
+}
\ No newline at end of file
diff --git a/math/.gitignore b/math/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/math/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/math/build.gradle.kts b/math/build.gradle.kts
new file mode 100644
index 00000000..2789ff6b
--- /dev/null
+++ b/math/build.gradle.kts
@@ -0,0 +1,13 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+kotlin {
+ compilerOptions.jvmTarget = JvmTarget.JVM_17
+}
\ No newline at end of file
diff --git a/math/src/main/java/com/icegps/io/util/NumberExt.kt b/math/src/main/java/com/icegps/io/util/NumberExt.kt
new file mode 100644
index 00000000..a44f6895
--- /dev/null
+++ b/math/src/main/java/com/icegps/io/util/NumberExt.kt
@@ -0,0 +1,75 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.icegps.io.util
+
+import kotlin.math.*
+
+//private fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this
+private val MINUS_ZERO_D = -0.0
+private fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this
+
+fun Double.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String {
+ if (this.isNanOrInfinite()) return this.toString()
+
+ //val bits = this.toRawBits()
+ //val sign = (bits ushr 63) != 0L
+ //val exponent = (bits ushr 52) and 0b11111111111
+ //val fraction = bits and ((1L shl 52) - 1L)
+
+ val res = this.roundDecimalPlaces(decimalPlaces).normalizeZero().toString()
+
+ val eup = res.indexOf('E')
+ val elo = res.indexOf('e')
+ val eIndex = if (eup >= 0) eup else elo
+ val rez = if (eIndex >= 0) {
+ val base = res.substring(0, eIndex)
+ val exp = res.substring(eIndex + 1).toInt()
+ val rbase = if (base.contains(".")) base else "$base.0"
+ val zeros = "0".repeat(exp.absoluteValue + 2)
+ val part = if (exp > 0) "$rbase$zeros" else "$zeros$rbase"
+ val pointIndex2 = part.indexOf(".")
+ val pointIndex = if (pointIndex2 < 0) part.length else pointIndex2
+ val outIndex = pointIndex + exp
+ val part2 = part.replace(".", "")
+ buildString {
+ if ((0 until outIndex).all { part2[it] == '0' }) {
+ append('0')
+ } else {
+ append(part2, 0, outIndex)
+ }
+ append('.')
+ append(part2, outIndex, part2.length)
+ }
+ } else {
+ res
+ }
+
+ val pointIndex = rez.indexOf('.')
+ val integral = if (pointIndex >= 0) rez.substring(0, pointIndex) else rez
+ if (decimalPlaces == 0) return integral
+
+ val decimal = if (pointIndex >= 0) rez.substring(pointIndex + 1).trimEnd('0') else ""
+ return buildString(2 + integral.length + decimalPlaces) {
+ append(integral)
+ if (decimal.isNotEmpty() || !skipTrailingZeros) {
+ val decimalCount = min(decimal.length, decimalPlaces)
+ val allZeros = (0 until decimalCount).all { decimal[it] == '0' }
+ if (!skipTrailingZeros || !allZeros) {
+ append('.')
+ append(decimal, 0, decimalCount)
+ if (!skipTrailingZeros) repeat(decimalPlaces - decimalCount) { append('0') }
+ }
+ }
+ }
+}
+
+fun Float.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String = this.toDouble().toStringDecimal(decimalPlaces, skipTrailingZeros)
+
+private fun Double.roundDecimalPlaces(places: Int): Double {
+ if (places < 0) return this
+ val placesFactor: Double = 10.0.pow(places.toDouble())
+ return round(this * placesFactor) / placesFactor
+}
+
+private fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite()
+private fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite()
diff --git a/math/src/main/java/com/icegps/io/util/NumberParser.kt b/math/src/main/java/com/icegps/io/util/NumberParser.kt
new file mode 100644
index 00000000..c9cab617
--- /dev/null
+++ b/math/src/main/java/com/icegps/io/util/NumberParser.kt
@@ -0,0 +1,78 @@
+package com.icegps.io.util
+
+import kotlin.math.*
+
+object NumberParser {
+ const val END = '\u0000'
+
+ fun parseInt(str: String, start: Int = 0, end: Int = str.length, radix: Int = 10): Int {
+ var n = start
+ return parseInt(radix) { if (n >= end) END else str[n++] }
+ }
+
+ fun parseDouble(str: String, start: Int = 0, end: Int = str.length): Double {
+ var n = start
+ return parseDouble { if (n >= end) END else str[n++] }
+ }
+
+ inline fun parseInt(radix: Int = 10, gen: (Int) -> Char): Int {
+ var positive = true
+ var out = 0
+ var n = 0
+ while (true) {
+ val c = gen(n++)
+ if (c == END) break
+ if (c == '-' || c == '+') {
+ positive = (c == '+')
+ } else {
+ val value = c.ctypeAsInt()
+ if (value < 0) break
+ out *= radix
+ out += value
+ }
+ }
+ return if (positive) out else -out
+ }
+
+ inline fun parseDouble(gen: (Int) -> Char): Double {
+ var out = 0.0
+ var frac = 1.0
+ var pointSeen = false
+ var eSeen = false
+ var negate = false
+ var negateExponent = false
+ var exponent = 0
+ var n = 0
+ while (true) {
+ val c = gen(n++)
+ if (c == END) break
+ when (c) {
+ 'e', 'E' -> eSeen = true
+ '-' -> {
+ if (eSeen) negateExponent = true else negate = true
+ }
+ '.' -> pointSeen = true
+ else -> {
+ if (eSeen) {
+ exponent *= 10
+ exponent += c.ctypeAsInt()
+ } else {
+ if (pointSeen) frac /= 10
+ out *= 10
+ out += c.ctypeAsInt()
+ }
+ }
+ }
+ }
+ val res = (out * frac) * 10.0.pow(if (negateExponent) -exponent else exponent)
+ return if (negate) -res else res
+ }
+}
+
+@Suppress("ConvertTwoComparisonsToRangeCheck") // @TODO: Kotlin-Native doesn't optimize ranges
+@PublishedApi internal fun Char.ctypeAsInt(): Int = when {
+ this >= '0' && this <= '9' -> this - '0'
+ this >= 'a' && this <= 'z' -> this - 'a' + 10
+ this >= 'A' && this <= 'Z' -> this - 'A' + 10
+ else -> -1
+}
diff --git a/math/src/main/java/com/icegps/math/Alignment.kt b/math/src/main/java/com/icegps/math/Alignment.kt
new file mode 100644
index 00000000..28b2eb1b
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Alignment.kt
@@ -0,0 +1,47 @@
+package com.icegps.math
+
+import kotlin.math.absoluteValue
+
+////////////////////
+////////////////////
+
+/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Int.nextAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align)
+/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Long.nextAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align)
+/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Float.nextAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align)
+/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Double.nextAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align)
+
+/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Int.prevAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
+/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Long.prevAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
+/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Float.prevAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
+/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Double.prevAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align
+
+/** Returns whether [this] is multiple of [alignment] */
+public fun Int.isAlignedTo(alignment: Int): Boolean = alignment == 0 || (this % alignment) == 0
+/** Returns whether [this] is multiple of [alignment] */
+public fun Long.isAlignedTo(alignment: Long): Boolean = alignment == 0L || (this % alignment) == 0L
+/** Returns whether [this] is multiple of [alignment] */
+public fun Float.isAlignedTo(alignment: Float): Boolean = alignment == 0f || (this % alignment) == 0f
+/** Returns whether [this] is multiple of [alignment] */
+public fun Double.isAlignedTo(alignment: Double): Boolean = alignment == 0.0 || (this % alignment) == 0.0
+
+/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Float.nearestAlignedTo(align: Float): Float {
+ val prev = this.prevAlignedTo(align)
+ val next = this.nextAlignedTo(align)
+ return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
+}
+/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */
+public fun Double.nearestAlignedTo(align: Double): Double {
+ val prev = this.prevAlignedTo(align)
+ val next = this.nextAlignedTo(align)
+ return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
+}
+
diff --git a/math/src/main/java/com/icegps/math/BooleanConversion.kt b/math/src/main/java/com/icegps/math/BooleanConversion.kt
new file mode 100644
index 00000000..9327a26f
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/BooleanConversion.kt
@@ -0,0 +1,9 @@
+package com.icegps.math
+
+////////////////////
+////////////////////
+
+/** Converts this [Boolean] into integer: 1 for true, 0 for false */
+inline fun Boolean.toInt(): Int = if (this) 1 else 0
+inline fun Boolean.toByte(): Byte = if (this) 1 else 0
+inline fun Byte.toBoolean(): Boolean = this.toInt() != 0
diff --git a/math/src/main/java/com/icegps/math/Clamp.kt b/math/src/main/java/com/icegps/math/Clamp.kt
new file mode 100644
index 00000000..c6317718
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Clamp.kt
@@ -0,0 +1,38 @@
+package com.icegps.math
+
+/** Clamps [this] value into the range [min] and [max] */
+fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this
+/** Clamps [this] value into the range [min] and [max] */
+fun Long.clamp(min: Long, max: Long): Long = if (this < min) min else if (this > max) max else this
+/** Clamps [this] value into the range [min] and [max] */
+fun Double.clamp(min: Double, max: Double): Double = if (this < min) min else if (this > max) max else this
+/** Clamps [this] value into the range [min] and [max] */
+fun Float.clamp(min: Float, max: Float): Float = if ((this < min)) min else if ((this > max)) max else this
+
+/** Clamps [this] value into the range 0 and 1 */
+fun Double.clamp01(): Double = clamp(0.0, 1.0)
+/** Clamps [this] value into the range 0 and 1 */
+fun Float.clamp01(): Float = clamp(0f, 1f)
+
+/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int]. The default parameters will cover the whole range of values. */
+fun Long.toIntClamp(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): Int {
+ if (this < min) return min
+ if (this > max) return max
+ return this.toInt()
+}
+
+/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int] (where [min] must be zero or positive). The default parameters will cover the whole range of positive and zero values. */
+fun Long.toUintClamp(min: Int = 0, max: Int = Int.MAX_VALUE): Int = this.toIntClamp(min, max)
+
+/** Clamps the integer value in the 0..255 range */
+fun Int.clampUByte(): Int {
+ val n = this and -(if (this >= 0) 1 else 0)
+ return (n or (0xFF - n shr 31)) and 0xFF
+}
+fun Int.clampUShort(): Int {
+ val n = this and -(if (this >= 0) 1 else 0)
+ return (n or (0xFFFF - n shr 31)) and 0xFFFF
+}
+
+fun Int.toShortClamped(): Short = this.clamp(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
+fun Int.toByteClamped(): Byte = this.clamp(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()
diff --git a/math/src/main/java/com/icegps/math/ConvertRange.kt b/math/src/main/java/com/icegps/math/ConvertRange.kt
new file mode 100644
index 00000000..864c799c
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/ConvertRange.kt
@@ -0,0 +1,24 @@
+package com.icegps.math
+
+////////////////////
+////////////////////
+
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
+fun Float.convertRange(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin)))
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
+fun Double.convertRange(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin)))
+//fun Double.convertRange(minSrc: Double, maxSrc: Double, minDst: Double, maxDst: Double): Double = (((this - minSrc) / (maxSrc - minSrc)) * (maxDst - minDst)) + minDst
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
+fun Int.convertRange(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toInt()
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */
+fun Long.convertRange(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toLong()
+
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
+fun Float.convertRangeClamped(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
+fun Double.convertRangeClamped(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
+fun Int.convertRangeClamped(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
+/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */
+fun Long.convertRangeClamped(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax)
+
diff --git a/math/src/main/java/com/icegps/math/Division.kt b/math/src/main/java/com/icegps/math/Division.kt
new file mode 100644
index 00000000..35994498
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Division.kt
@@ -0,0 +1,19 @@
+package com.icegps.math
+
+import kotlin.math.roundToInt
+
+////////////////////
+////////////////////
+
+/** Divides [this] into [that] rounding to the floor */
+public infix fun Int.divFloor(that: Int): Int = this / that
+/** Divides [this] into [that] rounding to the ceil */
+public infix fun Int.divCeil(that: Int): Int = if (this % that != 0) (this / that) + 1 else (this / that)
+/** Divides [this] into [that] rounding to the round */
+public infix fun Int.divRound(that: Int): Int = (this.toDouble() / that.toDouble()).roundToInt()
+
+public infix fun Long.divCeil(other: Long): Long {
+ val res = this / other
+ if (this % other != 0L) return res + 1
+ return res
+}
diff --git a/math/src/main/java/com/icegps/math/Fract.kt b/math/src/main/java/com/icegps/math/Fract.kt
new file mode 100644
index 00000000..3f9185ce
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Fract.kt
@@ -0,0 +1,4 @@
+package com.icegps.math
+
+public inline fun fract(value: Float): Float = value - value.toIntFloor()
+public inline fun fract(value: Double): Double = value - value.toIntFloor()
diff --git a/math/src/main/java/com/icegps/math/ILog.kt b/math/src/main/java/com/icegps/math/ILog.kt
new file mode 100644
index 00000000..0ca6c55a
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/ILog.kt
@@ -0,0 +1,9 @@
+package com.icegps.math
+
+////////////////////
+////////////////////
+
+/** Performs a fast integral logarithmic of base two */
+fun ilog2(v: Int): Int = if (v == 0) (-1) else (31 - v.countLeadingZeroBits())
+// fun ilog2(v: Int): Int = kotlin.math.log2(v.toDouble()).toInt()
+fun ilog2Ceil(v: Int): Int = kotlin.math.ceil(kotlin.math.log2(v.toDouble())).toInt()
diff --git a/math/src/main/java/com/icegps/math/IsAlmostEquals.kt b/math/src/main/java/com/icegps/math/IsAlmostEquals.kt
new file mode 100644
index 00000000..1ff18a92
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/IsAlmostEquals.kt
@@ -0,0 +1,14 @@
+package com.icegps.math
+
+import kotlin.math.*
+
+interface IsAlmostEquals {
+ fun isAlmostEquals(other: T, epsilon: Double = 0.000001): Boolean
+}
+
+interface IsAlmostEqualsF {
+ fun isAlmostEquals(other: T, epsilon: Float = 0.0001f): Boolean
+}
+
+fun Float.isAlmostEquals(other: Float, epsilon: Float = 0.000001f): Boolean = (this - other).absoluteValue < epsilon
+fun Double.isAlmostEquals(other: Double, epsilon: Double = 0.000001): Boolean = (this - other).absoluteValue < epsilon
diff --git a/math/src/main/java/com/icegps/math/IsAlmostZero.kt b/math/src/main/java/com/icegps/math/IsAlmostZero.kt
new file mode 100644
index 00000000..7ef8e7a6
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/IsAlmostZero.kt
@@ -0,0 +1,4 @@
+package com.icegps.math
+
+fun Double.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-19
+fun Float.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-6
diff --git a/math/src/main/java/com/icegps/math/IsEven.kt b/math/src/main/java/com/icegps/math/IsEven.kt
new file mode 100644
index 00000000..784e8b66
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/IsEven.kt
@@ -0,0 +1,9 @@
+package com.icegps.math
+
+////////////////////
+////////////////////
+
+/** Checks if [this] is odd (not multiple of two) */
+val Int.isOdd: Boolean get() = (this % 2) == 1
+/** Checks if [this] is even (multiple of two) */
+val Int.isEven: Boolean get() = (this % 2) == 0
diff --git a/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt b/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt
new file mode 100644
index 00000000..40449b6e
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt
@@ -0,0 +1,11 @@
+package com.icegps.math
+
+///** Check if [this] floating point value is not a number or infinite */
+//public fun Float.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite()
+///** Check if [this] floating point value is not a number or infinite */
+//public fun Double.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite()
+
+
+fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite()
+
+fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite()
diff --git a/math/src/main/java/com/icegps/math/Math.kt b/math/src/main/java/com/icegps/math/Math.kt
new file mode 100644
index 00000000..5dfc1e57
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Math.kt
@@ -0,0 +1,133 @@
+package com.icegps.math
+
+import kotlin.math.*
+
+const val PIF = PI.toFloat()
+const val PI2F = (PI * 2).toFloat()
+
+fun Double.betweenInclusive(min: Double, max: Double): Boolean = (this >= min) && (this <= max)
+
+fun almostEquals(a: Float, b: Float) = almostZero(a - b)
+fun almostZero(a: Float) = abs(a) <= 0.0000001
+
+fun almostEquals(a: Double, b: Double) = almostZero(a - b)
+fun almostZero(a: Double) = abs(a) <= 0.0000001
+
+fun isEquivalent(a: Double, b: Double, epsilon: Double = 0.0001): Boolean = (a - epsilon < b) && (a + epsilon > b)
+
+fun Double.smoothstep(edge0: Double, edge1: Double): Double {
+ if (this < edge0) return 0.0
+ if (this >= edge1) return 1.0
+ val v = ((this - edge0) / (edge1 - edge0))//.clamp(0.0, 1.0)
+ return v * v * (3 - 2 * v)
+}
+
+fun log(v: Int, base: Int): Int = log(v.toDouble(), base.toDouble()).toInt()
+fun ln(v: Int): Int = ln(v.toDouble()).toInt()
+fun log2(v: Int): Int = log(v.toDouble(), 2.0).toInt()
+fun log10(v: Int): Int = log(v.toDouble(), 10.0).toInt()
+
+@Deprecated("", ReplaceWith("v.squared()"))
+fun sq(v: Int): Int = v.squared()
+@Deprecated("", ReplaceWith("v.squared()"))
+fun sq(v: Float): Float = v.squared()
+@Deprecated("", ReplaceWith("v.squared()"))
+fun sq(v: Double): Double = v.squared()
+
+/** Signs of the value. Zero will be converted into -1 */
+val Int.signM1: Int get() = signNonZeroM1(this)
+/** Signs of the value. Zero will be converted into -1 */
+val Float.signM1: Float get() = signNonZeroM1(this).toFloat()
+/** Signs of the value. Zero will be converted into -1 */
+val Double.signM1: Double get() = signNonZeroM1(this).toDouble()
+
+/** Signs of the value. Zero will be converted into +1 */
+val Int.signP1: Int get() = signNonZeroP1(this)
+/** Signs of the value. Zero will be converted into +1 */
+val Float.signP1: Float get() = signNonZeroP1(this).toFloat()
+/** Signs of the value. Zero will be converted into +1 */
+val Double.signP1: Double get() = signNonZeroP1(this).toDouble()
+
+/** Signs of the value. Zero will be converted into -1 */
+fun signNonZeroM1(x: Int): Int = if (x <= 0) -1 else +1
+/** Signs of the value. Zero will be converted into -1 */
+fun signNonZeroM1(x: Float): Int = if (x <= 0) -1 else +1
+/** Signs of the value. Zero will be converted into -1 */
+fun signNonZeroM1(x: Double): Int = if (x <= 0) -1 else +1
+
+
+/** Signs of the value. Zero will be converted into +1 */
+fun signNonZeroP1(x: Int): Int = if (x >= 0) +1 else -1
+/** Signs of the value. Zero will be converted into +1 */
+fun signNonZeroP1(x: Float): Int = if (x >= 0) +1 else -1
+/** Signs of the value. Zero will be converted into +1 */
+fun signNonZeroP1(x: Double): Int = if (x >= 0) +1 else -1
+
+fun Float.normalizeAlmostZero() = if (this.isAlmostZero()) 0f else this
+
+fun Double.closestMultipleOf(multiple: Double): Double {
+ val prev = prevMultipleOf(multiple)
+ val next = nextMultipleOf(multiple)
+ return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
+}
+fun Int.closestMultipleOf(multiple: Int): Int {
+ val prev = prevMultipleOf(multiple)
+ val next = nextMultipleOf(multiple)
+ return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
+}
+fun Long.closestMultipleOf(multiple: Long): Long {
+ val prev = prevMultipleOf(multiple)
+ val next = nextMultipleOf(multiple)
+ return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next
+}
+
+fun Double.nextMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
+fun Int.nextMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
+fun Long.nextMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple)
+
+fun Double.prevMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
+fun Int.prevMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
+fun Long.prevMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple
+
+fun Double.isMultipleOf(multiple: Double) = multiple.isAlmostZero() || (this % multiple).isAlmostZero()
+fun Int.isMultipleOf(multiple: Int) = multiple == 0 || (this % multiple) == 0
+fun Long.isMultipleOf(multiple: Long) = multiple == 0L || (this % multiple) == 0L
+
+fun Double.squared(): Double = this * this
+fun Float.squared(): Float = this * this
+fun Int.squared(): Int = this * this
+
+fun min(a: Int, b: Int, c: Int) = min(min(a, b), c)
+fun min(a: Float, b: Float, c: Float) = min(min(a, b), c)
+fun min(a: Double, b: Double, c: Double) = min(min(a, b), c)
+
+fun min(a: Int, b: Int, c: Int, d: Int) = min(min(min(a, b), c), d)
+fun min(a: Float, b: Float, c: Float, d: Float) = min(min(min(a, b), c), d)
+fun min(a: Double, b: Double, c: Double, d: Double) = min(min(min(a, b), c), d)
+
+fun min(a: Int, b: Int, c: Int, d: Int, e: Int) = min(min(min(min(a, b), c), d), e)
+fun min(a: Float, b: Float, c: Float, d: Float, e: Float) = min(min(min(min(a, b), c), d), e)
+fun min(a: Double, b: Double, c: Double, d: Double, e: Double) = min(min(min(min(a, b), c), d), e)
+
+fun max(a: Int, b: Int, c: Int) = max(max(a, b), c)
+fun max(a: Float, b: Float, c: Float) = max(max(a, b), c)
+fun max(a: Double, b: Double, c: Double) = max(max(a, b), c)
+
+fun max(a: Int, b: Int, c: Int, d: Int) = max(max(max(a, b), c), d)
+fun max(a: Float, b: Float, c: Float, d: Float) = max(max(max(a, b), c), d)
+fun max(a: Double, b: Double, c: Double, d: Double) = max(max(max(a, b), c), d)
+
+fun max(a: Int, b: Int, c: Int, d: Int, e: Int) = max(max(max(max(a, b), c), d), e)
+fun max(a: Float, b: Float, c: Float, d: Float, e: Float) = max(max(max(max(a, b), c), d), e)
+fun max(a: Double, b: Double, c: Double, d: Double, e: Double) = max(max(max(max(a, b), c), d), e)
+
+////////////////////
+////////////////////
+
+
+// @TODO: Optimize this
+fun Int.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length
+fun Long.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length
+
+fun Int.cycle(min: Int, max: Int): Int = ((this - min) umod (max - min + 1)) + min
+fun Int.cycleSteps(min: Int, max: Int): Int = (this - min) / (max - min + 1)
diff --git a/math/src/main/java/com/icegps/math/NormalizeZero.kt b/math/src/main/java/com/icegps/math/NormalizeZero.kt
new file mode 100644
index 00000000..14dc9bf5
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/NormalizeZero.kt
@@ -0,0 +1,7 @@
+package com.icegps.math
+
+//fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this
+private val MINUS_ZERO_D = -0.0
+private val MINUS_ZERO_F = -0.0f
+fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this
+fun Float.normalizeZero(): Float = if (this == MINUS_ZERO_F) 0f else this
diff --git a/math/src/main/java/com/icegps/math/PowerOfTwo.kt b/math/src/main/java/com/icegps/math/PowerOfTwo.kt
new file mode 100644
index 00000000..6076d1b0
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/PowerOfTwo.kt
@@ -0,0 +1,22 @@
+package com.icegps.math
+
+
+/** Returns the next power of two of [this] */
+val Int.nextPowerOfTwo: Int get() {
+ var v = this
+ v--
+ v = v or (v shr 1)
+ v = v or (v shr 2)
+ v = v or (v shr 4)
+ v = v or (v shr 8)
+ v = v or (v shr 16)
+ v++
+ return v
+}
+/** Checks if [this] value is power of two */
+val Int.isPowerOfTwo: Boolean get() = this.nextPowerOfTwo == this
+
+/** Returns the previous power of two of [this] */
+val Int.prevPowerOfTwo: Int get() = if (isPowerOfTwo) this else (nextPowerOfTwo ushr 1)
+
+
diff --git a/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt b/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt
new file mode 100644
index 00000000..3598630f
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt
@@ -0,0 +1,16 @@
+package com.icegps.math
+
+import kotlin.math.*
+
+fun Float.roundDecimalPlaces(places: Int): Float {
+ if (places < 0) return this
+ val placesFactor: Float = 10f.pow(places.toFloat())
+ return round(this * placesFactor) / placesFactor
+}
+
+fun Double.roundDecimalPlaces(places: Int): Double {
+ if (places < 0) return this
+ val placesFactor: Double = 10.0.pow(places.toDouble())
+ return round(this * placesFactor) / placesFactor
+}
+
diff --git a/math/src/main/java/com/icegps/math/ToIntegerConverters.kt b/math/src/main/java/com/icegps/math/ToIntegerConverters.kt
new file mode 100644
index 00000000..333a9028
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/ToIntegerConverters.kt
@@ -0,0 +1,29 @@
+package com.icegps.math
+
+import kotlin.math.*
+
+////////////////////
+////////////////////
+
+/** Converts [this] into [Int] rounding to the ceiling */
+fun Float.toIntCeil(): Int = ceil(this).toInt()
+/** Converts [this] into [Int] rounding to the ceiling */
+fun Double.toIntCeil(): Int = ceil(this).toInt()
+
+/** Converts [this] into [Int] rounding to the nearest */
+fun Float.toIntRound(): Int = round(this).toInt()
+/** Converts [this] into [Int] rounding to the nearest */
+fun Double.toIntRound(): Int = round(this).toInt()
+
+/** Converts [this] into [Int] rounding to the nearest */
+fun Float.toLongRound(): Long = round(this).toLong()
+/** Converts [this] into [Int] rounding to the nearest */
+fun Double.toLongRound(): Long = round(this).toLong()
+
+/** Convert this [Long] into an [Int] but throws an [IllegalArgumentException] in the case that operation would produce an overflow */
+fun Long.toIntSafe(): Int = if (this in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) this.toInt() else throw IllegalArgumentException("Long doesn't fit Integer")
+
+/** Converts [this] into [Int] rounding to the floor */
+fun Float.toIntFloor(): Int = floor(this).toInt()
+/** Converts [this] into [Int] rounding to the floor */
+fun Double.toIntFloor(): Int = floor(this).toInt()
diff --git a/math/src/main/java/com/icegps/math/Umod.kt b/math/src/main/java/com/icegps/math/Umod.kt
new file mode 100644
index 00000000..14fd1c1a
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Umod.kt
@@ -0,0 +1,35 @@
+package com.icegps.math
+
+private val MINUS_ZERO_F = -0.0f
+
+////////////////////
+////////////////////
+
+/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */
+public infix fun Int.umod(other: Int): Int {
+ val rm = this % other
+ val remainder = if (rm == -0) 0 else rm
+ return when {
+ remainder < 0 -> remainder + other
+ else -> remainder
+ }
+}
+
+/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */
+public infix fun Double.umod(other: Double): Double {
+ val rm = this % other
+ val remainder = if (rm == -0.0) 0.0 else rm
+ return when {
+ remainder < 0.0 -> remainder + other
+ else -> remainder
+ }
+}
+
+public infix fun Float.umod(other: Float): Float {
+ val rm = this % other
+ val remainder = if (rm == MINUS_ZERO_F) 0f else rm
+ return when {
+ remainder < 0f -> remainder + other
+ else -> remainder
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/Unsigned.kt b/math/src/main/java/com/icegps/math/Unsigned.kt
new file mode 100644
index 00000000..13c2ea60
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/Unsigned.kt
@@ -0,0 +1,13 @@
+package com.icegps.math
+
+////////////////////
+////////////////////
+
+/** Returns an [Int] representing this [Byte] as if it was unsigned 0x00..0xFF */
+inline val Byte.unsigned: Int get() = this.toInt() and 0xFF
+
+/** Returns an [Int] representing this [Short] as if it was unsigned 0x0000..0xFFFF */
+inline val Short.unsigned: Int get() = this.toInt() and 0xFFFF
+
+/** Returns a [Long] representing this [Int] as if it was unsigned 0x00000000L..0xFFFFFFFFL */
+inline val Int.unsigned: Long get() = this.toLong() and 0xFFFFFFFFL
diff --git a/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt b/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt
new file mode 100644
index 00000000..45107370
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt
@@ -0,0 +1,41 @@
+@file:Suppress("PackageDirectoryMismatch")
+
+package com.icegps.math.annotations
+
+@DslMarker
+@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
+annotation class KorDslMarker
+
+@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
+@DslMarker
+annotation class ViewDslMarker
+
+@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
+@DslMarker
+annotation class RootViewDslMarker
+
+@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
+@DslMarker
+annotation class VectorDslMarker
+
+@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
+annotation class KormaExperimental(val reason: String = "")
+
+//@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
+/**
+ * Mutable APIs follow the following convention:
+ *
+ * ```kotlin
+ * interface IType { val ... }
+ * class MType : IType(override var ...) : IType
+ * ```
+ *
+ * Then in usage places:
+ *
+ * ```kotlin
+ * fun doSomethingWith(a: IType, out: MType = MType()): MType
+ * ```
+ *
+ * This convention supports allocation-free APIs by being able to preallocate instances and passing them as the output.
+ */
+annotation class KormaMutableApi
diff --git a/math/src/main/java/com/icegps/math/geometry/AABB3D.kt b/math/src/main/java/com/icegps/math/geometry/AABB3D.kt
new file mode 100644
index 00000000..ea4fe92e
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/AABB3D.kt
@@ -0,0 +1,55 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.geometry.shape.*
+import kotlin.math.*
+
+data class AABB3D(val min: Vector3F = Vector3F(), val max: Vector3F = Vector3F()) : SimpleShape3D {
+ val minX: Float get() = min.x
+ val minY: Float get() = min.y
+ val minZ: Float get() = min.z
+
+ val maxX: Float get() = max.x
+ val maxY: Float get() = max.y
+ val maxZ: Float get() = max.z
+
+ val sizeX: Float get() = maxX - minX
+ val sizeY: Float get() = maxY - minY
+ val sizeZ: Float get() = maxZ - minZ
+
+ companion object {
+ operator fun invoke(min: Float = Float.POSITIVE_INFINITY, max: Float = Float.NEGATIVE_INFINITY): AABB3D =
+ AABB3D(Vector3F(min, min, min), Vector3F(max, max, max))
+
+ fun fromSphere(pos: Vector3F, radius: Float): AABB3D = AABB3D(
+ Vector3F(pos.x - radius, pos.y - radius, pos.z - radius),
+ Vector3F(pos.x + radius, pos.y + radius, pos.z + radius)
+ )
+ }
+
+ fun expandedToFit(that: AABB3D): AABB3D {
+ val a = this
+ val b = that
+ return AABB3D(
+ min = Vector3F(min(a.minX, b.minX), min(a.minY, b.minY), min(a.minZ, b.minZ)),
+ max = Vector3F(max(a.maxX, b.maxX), max(a.maxY, b.maxY), max(a.maxZ, b.maxZ)),
+ )
+ }
+
+ fun intersectsSphere(sphere: Sphere3D): Boolean = intersectsSphere(sphere.center, sphere.radius)
+ fun intersectsSphere(origin: Vector3F, radius: Float): Boolean = !(origin.x + radius < minX ||
+ origin.y + radius < minY ||
+ origin.z + radius < minZ ||
+ origin.x - radius > maxX ||
+ origin.y - radius > maxY ||
+ origin.z - radius > maxZ)
+
+ fun intersectsAABB(box: AABB3D): Boolean = max.x > box.min.x && min.x < box.max.x &&
+ max.y > box.min.y && min.y < box.max.y &&
+ max.z > box.min.z && min.z < box.max.z
+
+ override val center: Vector3F get() = (min + max) * 0.5f
+ override val volume: Float get() {
+ val v = (max - min)
+ return v.x * v.y * v.z
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Anchor.kt b/math/src/main/java/com/icegps/math/geometry/Anchor.kt
new file mode 100644
index 00000000..108a655a
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Anchor.kt
@@ -0,0 +1,103 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.interpolation.*
+
+typealias Anchor = Anchor2D
+typealias Anchor3 = Anchor3F
+
+data class Anchor2D(val sx: Double, val sy: Double) : Interpolable {
+ fun toVector(): Vector2D = Vector2D(sx, sy)
+
+ val ratioX: Ratio get() = sx.toRatio()
+ val ratioY: Ratio get() = sy.toRatio()
+
+ constructor(sx: Float, sy: Float) : this(sx.toDouble(), sy.toDouble())
+ constructor(sx: Int, sy: Int) : this(sx.toDouble(), sy.toDouble())
+
+ inline fun withX(sx: Number): Anchor = Anchor(sx.toDouble(), sy)
+ inline fun withY(sy: Number): Anchor = Anchor(sx, sy.toDouble())
+
+ inline fun withX(ratioX: Ratio): Anchor = Anchor(ratioX.toDouble(), sy)
+ inline fun withY(ratioY: Ratio): Anchor = Anchor(sx, ratioY.toDouble())
+
+ companion object {
+ inline operator fun invoke(sx: Ratio, sy: Ratio): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble())
+ inline operator fun invoke(sx: Number, sy: Number): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble())
+
+ val TOP_LEFT: Anchor = Anchor(0f, 0f)
+ val TOP_CENTER: Anchor = Anchor(.5f, 0f)
+ val TOP_RIGHT: Anchor = Anchor(1f, 0f)
+
+ val MIDDLE_LEFT: Anchor = Anchor(0f, .5f)
+ val MIDDLE_CENTER: Anchor = Anchor(.5f, .5f)
+ val MIDDLE_RIGHT: Anchor = Anchor(1f, .5f)
+
+ val BOTTOM_LEFT: Anchor = Anchor(0f, 1f)
+ val BOTTOM_CENTER: Anchor = Anchor(.5f, 1f)
+ val BOTTOM_RIGHT: Anchor = Anchor(1f, 1f)
+
+ val TOP: Anchor get() = TOP_CENTER
+ val LEFT: Anchor get() = MIDDLE_LEFT
+ val RIGHT: Anchor get() = MIDDLE_RIGHT
+ val BOTTOM: Anchor get() = BOTTOM_CENTER
+ val CENTER: Anchor get() = MIDDLE_CENTER
+ }
+
+ override fun interpolateWith(ratio: Ratio, other: Anchor): Anchor = Anchor(
+ ratio.interpolate(this.sx, other.sx),
+ ratio.interpolate(this.sy, other.sy)
+ )
+
+ fun toNamedString(): String = when (this) {
+ TOP_LEFT -> "Anchor.TOP_LEFT"
+ TOP -> "Anchor.TOP"
+ TOP_RIGHT -> "Anchor.TOP_RIGHT"
+ LEFT -> "Anchor.LEFT"
+ CENTER -> "Anchor.MIDDLE_CENTER"
+ RIGHT -> "Anchor.RIGHT"
+ BOTTOM_LEFT -> "Anchor.BOTTOM_LEFT"
+ BOTTOM_CENTER -> "Anchor.BOTTOM_CENTER"
+ BOTTOM_RIGHT -> "Anchor.BOTTOM_RIGHT"
+ else -> toString()
+ }
+}
+
+operator fun Size.times(anchor: Anchor): Point = this.toVector() * anchor.toVector()
+//operator fun SizeInt.times(anchor: Anchor): PointInt = (this.toVector().toFloat() * anchor.toVector()).toInt()
+
+data class Anchor3F(val sx: Float, val sy: Float, val sz: Float) : Interpolable {
+ fun toVector(): Vector3F = Vector3F(sx, sy, sz)
+
+ val floatX: Float get() = sx
+ val floatY: Float get() = sy
+ val floatZ: Float get() = sz
+
+ val doubleX: Double get() = sx.toDouble()
+ val doubleY: Double get() = sy.toDouble()
+ val doubleZ: Double get() = sz.toDouble()
+
+ val ratioX: Ratio get() = sx.toRatio()
+ val ratioY: Ratio get() = sy.toRatio()
+ val ratioZ: Ratio get() = sz.toRatio()
+
+ constructor(sx: Double, sy: Double, sz: Double) : this(sx.toFloat(), sy.toFloat(), sz.toFloat())
+ constructor(sx: Int, sy: Int, sz: Int) : this(sx.toFloat(), sy.toFloat(), sz.toFloat())
+
+ fun withX(sx: Float): Anchor3F = Anchor3F(sx, sy, sz)
+ fun withX(sx: Int): Anchor3F = Anchor3F(sx.toFloat(), sy, sz)
+ fun withX(sx: Double): Anchor3F = Anchor3F(sx.toFloat(), sy, sz)
+
+ fun withY(sy: Float): Anchor3F = Anchor3F(sx, sy, sz)
+ fun withY(sy: Int): Anchor3F = Anchor3F(sx, sy.toFloat(), sz)
+ fun withY(sy: Double): Anchor3F = Anchor3F(sx, sy.toFloat(), sz)
+
+ fun withZ(sz: Float): Anchor3F = Anchor3F(sx, sy, sz)
+ fun withZ(sz: Int): Anchor3F = Anchor3F(sx, sy, sz.toFloat())
+ fun withZ(sz: Double): Anchor3F = Anchor3F(sx, sy, sz.toFloat())
+
+ override fun interpolateWith(ratio: Ratio, other: Anchor3F): Anchor3F = Anchor3F(
+ ratio.interpolate(this.sx, other.sx),
+ ratio.interpolate(this.sy, other.sy),
+ ratio.interpolate(this.sz, other.sz),
+ )
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Angle.kt b/math/src/main/java/com/icegps/math/geometry/Angle.kt
new file mode 100644
index 00000000..534e8e9e
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Angle.kt
@@ -0,0 +1,250 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.interpolation.*
+import com.icegps.math.range.*
+import com.icegps.number.*
+import kotlin.math.*
+
+@PublishedApi internal const val PI2 = PI * 2.0
+@PublishedApi internal const val DEG2RAD = PI / 180.0
+@PublishedApi internal const val RAD2DEG = 180.0 / PI
+
+@PublishedApi internal fun Angle_shortDistanceTo(from: Angle, to: Angle): Angle {
+ val r0 = from.ratio.toDouble() umod 1.0
+ val r1 = to.ratio.toDouble() umod 1.0
+ val diff = (r1 - r0 + 0.5) % 1.0 - 0.5
+ return if (diff < -0.5) Angle.fromRatio(diff + 1.0) else Angle.fromRatio(diff)
+}
+
+@PublishedApi internal fun Angle_longDistanceTo(from: Angle, to: Angle): Angle {
+ val short = Angle_shortDistanceTo(from, to)
+ return when {
+ short == Angle.ZERO -> Angle.ZERO
+ short < Angle.ZERO -> Angle.FULL + short
+ else -> -Angle.FULL + short
+ }
+}
+
+@PublishedApi internal fun Angle_between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle {
+ val angle = Angle.atan2(y1 - y0, x1 - x0)
+ return (if (angle < Angle.ZERO) angle + Angle.FULL else angle).adjustFromUp(up)
+}
+
+@PublishedApi internal fun Angle.adjustFromUp(up: Vector2D): Angle {
+ Orientation.checkValidUpVector(up)
+ return if (up.y > 0) this else -this
+}
+
+/**
+ * Represents an [Angle], [ratio] is in [0, 1] range, [radians] is in [0, 2PI] range, and [degrees] in [0, 360] range
+ * The internal representation is in [0, 1] range to reduce rounding errors, since floating points can represent
+ * a lot of values in that range.
+ *
+ * The equivalent old [Angle] constructor is now [Angle.fromRadians]
+ *
+ * Angles advance counter-clock-wise, starting with 0.degrees representing the right vector:
+ *
+ * Depending on what the up vector means, then numeric values of sin might be negated.
+ *
+ * 0.degrees represent right: up=Vector2.UP: cos =+1, sin= 0 || up=Vector2.UP_SCREEN: cos =+1, sin= 0
+ * 90.degrees represents up: up=Vector2.UP: cos = 0, sin=+1 || up=Vector2.UP_SCREEN: cos = 0, sin=-1
+ * 180.degrees represents left: up=Vector2.UP: cos =-1, sin= 0 || up=Vector2.UP_SCREEN: cos =-1, sin= 0
+ * 270.degrees represents down: up=Vector2.UP: cos = 0, sin=-1 || up=Vector2.UP_SCREEN: cos = 0, sin=+1
+ */
+//@KormaValueApi
+inline class Angle @PublishedApi internal constructor(
+ /** [0..1] ratio -> [0..360] degrees */
+ val radians: Double
+) : Comparable, IsAlmostEquals {
+ @PublishedApi inline internal val internal: Double get() = radians
+
+ /** [0..PI * 2] radians -> [0..360] degrees */
+ val ratio: Ratio get() = radiansToRatio(radians)
+ /** [0..360] degrees -> [0..PI * 2] radians -> [0..1] ratio */
+ val degrees: Double get() = radiansToDegrees(radians)
+
+ val cosine: Double get() = kotlin.math.cos(radians)
+ val sine: Double get() = kotlin.math.sin(radians)
+ val tangent: Double get() = kotlin.math.tan(radians)
+
+ fun cosine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).cosine
+ fun sine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).sine
+ fun tangent(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).tangent
+
+ val absoluteValue: Angle get() = Angle(internal.absoluteValue)
+ fun shortDistanceTo(other: Angle): Angle = Angle.shortDistanceTo(this, other)
+ fun longDistanceTo(other: Angle): Angle = Angle.longDistanceTo(this, other)
+
+ operator fun times(scale: Double): Angle = Angle(this.internal * scale)
+ operator fun div(scale: Double): Angle = Angle(this.internal / scale)
+ operator fun times(scale: Float): Angle = Angle(this.internal * scale)
+ operator fun div(scale: Float): Angle = Angle(this.internal / scale)
+ operator fun times(scale: Int): Angle = Angle(this.internal * scale)
+ operator fun div(scale: Int): Angle = Angle(this.internal / scale)
+ operator fun rem(angle: Angle): Angle = Angle(this.internal % angle.internal)
+ infix fun umod(angle: Angle): Angle = Angle(this.internal umod angle.internal)
+
+ operator fun div(other: Angle): Double = this.internal / other.internal // Ratio
+ operator fun plus(other: Angle): Angle = Angle(this.internal + other.internal)
+ operator fun minus(other: Angle): Angle = Angle(this.internal - other.internal)
+ operator fun unaryMinus(): Angle = Angle(-internal)
+ operator fun unaryPlus(): Angle = Angle(+internal)
+
+ fun inBetweenInclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = true)
+ fun inBetweenExclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = false)
+
+ infix fun inBetween(range: ClosedRange): Boolean = inBetween(range.start, range.endInclusive, inclusive = true)
+ infix fun inBetween(range: OpenRange): Boolean = inBetween(range.start, range.endExclusive, inclusive = false)
+
+ fun inBetween(min: Angle, max: Angle, inclusive: Boolean): Boolean {
+ val nthis = this.normalized
+ val nmin = min.normalized
+ val nmax = max.normalized
+ @Suppress("ConvertTwoComparisonsToRangeCheck")
+ return when {
+ nmin > nmax -> nthis >= nmin || (if (inclusive) nthis <= nmax else nthis < nmax)
+ else -> nthis >= nmin && (if (inclusive) nthis <= nmax else nthis < nmax)
+ }
+ }
+
+ override fun isAlmostEquals(other: Angle, epsilon: Double): Boolean = this.radians.isAlmostEquals(other.radians, epsilon)
+ fun isAlmostZero(epsilon: Double = 0.001): Boolean = isAlmostEquals(ZERO, epsilon)
+
+ /** Normalize between 0..1 ... 0..(PI*2).radians ... 0..360.degrees */
+ val normalized: Angle get() = fromRatio(ratio.toDouble() umod 1.0)
+ /** Normalize between -.5..+.5 ... -PI..+PI.radians ... -180..+180.degrees */
+ val normalizedHalf: Angle get() {
+ val res = normalized
+ return if (res > Angle.HALF) -Angle.FULL + res else res
+ }
+
+ override operator fun compareTo(other: Angle): Int = this.ratio.compareTo(other.ratio)
+
+ //override fun compareTo(other: Angle): Int {
+ // //return this.radians.compareTo(other.radians) // @TODO: Double.compareTo calls EnterFrame/LeaveFrame! because it uses a Double companion object
+ // val left = this.ratio
+ // val right = other.ratio
+ // // @TODO: Handle infinite/NaN? Though usually this won't happen
+ // if (left < right) return -1
+ // if (left > right) return +1
+ // return 0
+ //}
+
+ override fun toString(): String = "${degrees.roundDecimalPlaces(2).niceStr}.degrees"
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ companion object {
+ val EPSILON = Angle.fromRatio(0.00001)
+ val ZERO = Angle.fromRatio(0.0)
+ val QUARTER = Angle.fromRatio(0.25)
+ val HALF = Angle.fromRatio(0.5)
+ val THREE_QUARTERS = Angle.fromRatio(0.75)
+ val FULL = Angle.fromRatio(1.0)
+
+ inline fun fromRatio(ratio: Float): Angle = Angle(ratioToRadians(ratio.toRatio()))
+ inline fun fromRatio(ratio: Double): Angle = Angle(ratioToRadians(ratio.toRatio()))
+ inline fun fromRatio(ratio: Ratio): Angle = Angle(ratioToRadians(ratio))
+
+ inline fun fromRadians(radians: Double): Angle = Angle(radians)
+ inline fun fromRadians(radians: Float) = Angle(radians.toDouble())
+ inline fun fromRadians(radians: Int) = Angle(radians.toDouble())
+
+ inline fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees))
+ inline fun fromDegrees(degrees: Float) = Angle(degreesToRadians(degrees.toDouble()))
+ inline fun fromDegrees(degrees: Int) = Angle(degreesToRadians(degrees.toDouble()))
+
+ @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).cosineD"))
+ inline fun cos01(ratio: Double): Double = Angle.fromRatio(ratio).cosine
+ @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).sineD"))
+ inline fun sin01(ratio: Double): Double = Angle.fromRatio(ratio).sine
+ @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).tangentD"))
+ inline fun tan01(ratio: Double): Double = Angle.fromRatio(ratio).tangent
+
+ inline fun atan2(x: Float, y: Float, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
+ inline fun atan2(x: Double, y: Double, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
+ inline fun atan2(p: Point, up: Vector2D = Vector2D.UP): Angle = atan2(p.x, p.y, up)
+
+ inline fun asin(v: Double): Angle = kotlin.math.asin(v).radians
+ inline fun asin(v: Float): Angle = kotlin.math.asin(v).radians
+
+ inline fun acos(v: Double): Angle = kotlin.math.acos(v).radians
+ inline fun acos(v: Float): Angle = kotlin.math.acos(v).radians
+
+ fun arcCosine(v: Double): Angle = kotlin.math.acos(v).radians
+ fun arcCosine(v: Float): Angle = kotlin.math.acos(v).radians
+
+ fun arcSine(v: Double): Angle = kotlin.math.asin(v).radians
+ fun arcSine(v: Float): Angle = kotlin.math.asin(v).radians
+
+ fun arcTangent(x: Double, y: Double): Angle = kotlin.math.atan2(x, y).radians
+ fun arcTangent(x: Float, y: Float): Angle = kotlin.math.atan2(x, y).radians
+ fun arcTangent(v: Vector2F): Angle = kotlin.math.atan2(v.x, v.y).radians
+
+ inline fun ratioToDegrees(ratio: Ratio): Double = ratio * 360.0
+ inline fun ratioToRadians(ratio: Ratio): Double = ratio * PI2
+
+ inline fun degreesToRatio(degrees: Double): Ratio = Ratio(degrees / 360.0)
+ inline fun degreesToRadians(degrees: Double): Double = degrees * DEG2RAD
+
+ inline fun radiansToRatio(radians: Double): Ratio = Ratio(radians / PI2)
+ inline fun radiansToDegrees(radians: Double): Double = radians * RAD2DEG
+
+ inline fun shortDistanceTo(from: Angle, to: Angle): Angle = Angle_shortDistanceTo(from, to)
+ inline fun longDistanceTo(from: Angle, to: Angle): Angle = Angle_longDistanceTo(from, to)
+ inline fun between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle = Angle_between(x0, y0, x1, y1, up)
+
+ inline fun between(x0: Int, y0: Int, x1: Int, y1: Int, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)
+ inline fun between(x0: Float, y0: Float, x1: Float, y1: Float, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)
+
+ inline fun between(p0: Point, p1: Point, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)
+ inline fun between(p0: Vector2F, p1: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)
+
+ inline fun between(ox: Double, oy: Double, x1: Double, y1: Double, x2: Double, y2: Double, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)
+ inline fun between(ox: Float, oy: Float, x1: Float, y1: Float, x2: Float, y2: Float, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)
+
+ inline fun between(o: Point, v1: Point, v2: Point, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
+ inline fun between(o: Vector2F, v1: Vector2F, v2: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
+ }
+}
+
+inline fun cos(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.cosine(up)
+inline fun sin(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.sine(up)
+inline fun tan(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.tangent(up)
+
+inline fun cosf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.cosine(up).toFloat()
+inline fun sinf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.sine(up).toFloat()
+inline fun tanf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.tangent(up).toFloat()
+
+inline fun abs(angle: Angle): Angle = angle.absoluteValue
+inline fun min(a: Angle, b: Angle): Angle = Angle(min(a.internal, b.internal))
+inline fun max(a: Angle, b: Angle): Angle = Angle(max(a.internal, b.internal))
+
+fun Angle.clamp(min: Angle, max: Angle): Angle = min(max(this, min), max)
+
+operator fun ClosedRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endInclusive, inclusive = true)
+operator fun OpenRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endExclusive, inclusive = false)
+infix fun Angle.until(other: Angle): OpenRange = OpenRange(this, other)
+
+val Double.degrees: Angle get() = Angle.fromDegrees(this)
+val Double.radians: Angle get() = Angle.fromRadians(this)
+val Int.degrees: Angle get() = Angle.fromDegrees(this)
+val Int.radians: Angle get() = Angle.fromRadians(this)
+val Float.degrees: Angle get() = Angle.fromDegrees(this)
+val Float.radians: Angle get() = Angle.fromRadians(this)
+
+fun Ratio.interpolateAngle(l: Angle, r: Angle, minimizeAngle: Boolean): Angle = _interpolateAngleAny(this, l, r, minimizeAngle)
+fun Ratio.interpolateAngle(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true)
+fun Ratio.interpolateAngleNormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true)
+fun Ratio.interpolateAngleDenormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = false)
+
+private fun _interpolateAngleAny(ratio: Ratio, l: Angle, r: Angle, minimizeAngle: Boolean = true): Angle {
+ if (!minimizeAngle) return Angle.fromRatio(ratio.interpolate(l.ratio, r.ratio))
+ val ln = l.normalized
+ val rn = r.normalized
+ return when {
+ (rn - ln).absoluteValue <= Angle.HALF -> Angle.fromRadians(ratio.interpolate(ln.radians, rn.radians))
+ ln < rn -> Angle.fromRadians(ratio.interpolate((ln + Angle.FULL).radians, rn.radians)).normalized
+ else -> Angle.fromRadians(ratio.interpolate(ln.radians, (rn + Angle.FULL).radians)).normalized
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt b/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt
new file mode 100644
index 00000000..6409618c
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt
@@ -0,0 +1,60 @@
+package com.icegps.math.geometry
+
+inline class BoundsBuilder(val bounds: Rectangle) {
+ val isEmpty: Boolean get() = bounds.isNIL
+ val isNotEmpty: Boolean get() = bounds.isNotNIL
+
+ val xmin: Double get() = kotlin.math.min(bounds.left, bounds.right)
+ val xmax: Double get() = kotlin.math.max(bounds.left, bounds.right)
+ val ymin: Double get() = kotlin.math.min(bounds.top, bounds.bottom)
+ val ymax: Double get() = kotlin.math.max(bounds.top, bounds.bottom)
+
+ /** Minimum value found for X. [default] if ![hasPoints] */
+ fun xminOr(default: Double = 0.0): Double = if (hasPoints) xmin else default
+ /** Maximum value found for X. [default] if ![hasPoints] */
+ fun xmaxOr(default: Double = 0.0): Double = if (hasPoints) xmax else default
+ /** Minimum value found for Y. [default] if ![hasPoints] */
+ fun yminOr(default: Double = 0.0): Double = if (hasPoints) ymin else default
+ /** Maximum value found for Y. [default] if ![hasPoints] */
+ fun ymaxOr(default: Double = 0.0): Double = if (hasPoints) ymax else default
+
+ val hasPoints: Boolean get() = isNotEmpty
+
+ companion object {
+ val EMPTY = BoundsBuilder(Rectangle.NIL)
+
+ operator fun invoke(): BoundsBuilder = EMPTY
+ operator fun invoke(p1: Point): BoundsBuilder = BoundsBuilder(Rectangle(p1, Size(0, 0)))
+ operator fun invoke(p1: Point, p2: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2), Point.maxComponents(p1, p2)))
+ operator fun invoke(p1: Point, p2: Point, p3: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3), Point.maxComponents(p1, p2, p3)))
+ operator fun invoke(p1: Point, p2: Point, p3: Point, p4: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3, p4), Point.maxComponents(p1, p2, p3, p4)))
+ operator fun invoke(size: Int, func: BoundsBuilder.(Int) -> BoundsBuilder): BoundsBuilder {
+ var bb = BoundsBuilder()
+ for (n in 0 until size) bb = func(bb, n)
+ return bb
+ }
+ }
+ fun plus(x: Double, y: Double): BoundsBuilder = this.plus(Point(x, y))
+ operator fun plus(p: Point): BoundsBuilder {
+ if (bounds.isNIL) return BoundsBuilder(Rectangle(p, Size(0, 0)))
+ return BoundsBuilder(Rectangle.fromBounds(Point.minComponents(bounds.topLeft, p), Point.maxComponents(bounds.bottomRight, p)))
+ }
+ operator fun plus(bb: BoundsBuilder): BoundsBuilder = this + bb.bounds
+ operator fun plus(rect: Rectangle?): BoundsBuilder {
+ if (rect == null) return this
+ if (rect.isNIL) return this
+ return this + rect.topLeft + rect.bottomRight
+ }
+ operator fun plus(p: IPointList): BoundsBuilder {
+ var bb = this
+ for (n in 0 until p.size) bb = bb.plus(p[n])
+ return bb
+ }
+ //operator fun plus(rect: Rectangle): BoundsBuilder = TODO()
+ operator fun plus(rects: List): BoundsBuilder {
+ var bb = this
+ for (it in rects) bb += it
+ return bb
+ }
+ fun boundsOrNull(): Rectangle? = if (isEmpty) null else bounds
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Circle.kt b/math/src/main/java/com/icegps/math/geometry/Circle.kt
new file mode 100644
index 00000000..587f6318
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Circle.kt
@@ -0,0 +1,29 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.geometry.shape.*
+import kotlin.math.*
+
+data class Circle(override val center: Point, val radius: Double) : SimpleShape2D {
+ companion object {
+ inline operator fun invoke(center: Point, radius: Number) = Circle(center, radius.toDouble())
+ inline operator fun invoke(x: Number, y: Number, radius: Number) = Circle(Point(x.toDouble(), y.toDouble()), radius.toDouble())
+ }
+
+ override val closed: Boolean get() = true
+
+ override val area: Double get() = (PI * radius * radius)
+ override val perimeter: Double get() = (PI * 2.0 * radius)
+ override fun distance(p: Point): Double = (p - center).length - radius
+ override fun normalVectorAt(p: Point): Vector2D = (p - center).normalized
+
+ val radiusSquared: Double get() = radius * radius
+
+ fun distanceToCenterSquared(p: Point): Double = Point.distanceSquared(p, center)
+ // @TODO: Check if inside the circle
+ fun distanceClosestSquared(p: Point): Double = distanceToCenterSquared(p) - radiusSquared
+ // @TODO: Check if inside the circle
+ fun distanceFarthestSquared(p: Point): Double = distanceToCenterSquared(p) + radiusSquared
+ override fun projectedPoint(p: Point): Point = Point.polar(center, Angle.between(center, p), radius)
+ override fun containsPoint(p: Point): Boolean = (p - center).length <= radius
+ override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius, center.y - radius, center.x + radius, center.y + radius,)
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Ellipse.kt b/math/src/main/java/com/icegps/math/geometry/Ellipse.kt
new file mode 100644
index 00000000..5bd07078
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Ellipse.kt
@@ -0,0 +1,83 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.geometry.shape.*
+import kotlin.math.*
+
+data class Ellipse(override val center: Point, val radius: Size) : SimpleShape2D {
+ override val area: Double get() = (PI * radius.width * radius.height)
+ override val perimeter: Double get() {
+ if (radius.width == radius.height) return (PI * 2.0 * radius.width) // Circle formula
+ val (a, b) = radius
+ val h = ((a - b) * (a - b)) / ((a + b) * (a + b))
+ return (PI * (a + b) * (1 + ((3 * h) / (10 + sqrt(4 - (3 * h))))))
+ }
+
+ override fun distance(p: Point): Double {
+ val p = p - center
+ val scaledPoint = Vector2D(p.x / radius.width, p.y / radius.height)
+ val length = scaledPoint.length
+ return (length - 1) * min(radius.width, radius.height)
+ }
+
+ override fun normalVectorAt(p: Point): Vector2D {
+ val pointOnEllipse = p - center
+ val (a, b) = radius
+ val normal = Vector2D(pointOnEllipse.x / (a * a), pointOnEllipse.y / (b * b))
+ return normal.normalized
+ //val d = p - center
+ //val r2 = radius.toVector() * radius.toVector()
+ //return (d / r2).normalized
+ }
+
+ override fun projectedPoint(p: Point): Point {
+ val angle = Angle.between(center, p)
+ return center + Point(radius.width * angle.cosine, radius.height * angle.sine)
+
+ //val k = (radius.width * radius.height) / sqrt()
+ //return projectPointOntoEllipse(p, center, radius.toVector())
+ }
+
+ override fun containsPoint(p: Point): Boolean {
+ if (radius.isEmpty()) return false
+ // Check if the point is inside the ellipse using the ellipse equation:
+ // (x - centerX)^2 / radiusX^2 + (y - centerY)^2 / radiusY^2 <= 1
+ return ((p.x - center.x).pow(2) / radius.width.pow(2)) + ((p.y - center.y).pow(2) / radius.height.pow(2)) <= 1
+ }
+
+ override val closed: Boolean get() = true
+ override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius.width, center.y - radius.height, center.x + radius.width, center.y + radius.height)
+
+ companion object {
+ private fun projectPointOntoEllipse(point: Vector2F, center: Vector2F, radius: Vector2F, tolerance: Double = 1e-6, maxIterations: Int = 100): Vector2F {
+ var currentPoint = point
+ var i = 0
+
+ while (i < maxIterations) {
+ val dx = currentPoint.x - center.x
+ val dy = currentPoint.y - center.y
+ val rx2 = radius.x * radius.x
+ val ry2 = radius.y * radius.y
+
+ val f = Vector2F(
+ (dx * rx2 - dy * dx * dy) / (rx2 * ry2),
+ (dy * ry2 - dx * dy * dx) / (rx2 * ry2)
+ )
+
+ val df = Vector2F(
+ (ry2 - 2.0 * dy * dy) / (rx2 * ry2),
+ (rx2 - 2.0 * dx * dx) / (rx2 * ry2)
+ )
+
+ val nextPoint = currentPoint - f / df
+ val dist = (nextPoint - currentPoint).length
+
+ if (dist < tolerance) return nextPoint
+
+ currentPoint = nextPoint
+ i++
+ }
+
+ return currentPoint
+ }
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt b/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt
new file mode 100644
index 00000000..796d3f77
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt
@@ -0,0 +1,330 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import kotlin.math.*
+
+/**
+ * Rotations around Z axis, then X axis, then Y axis in that order.
+ */
+inline class EulerRotation private constructor(val data: Vector4F) : IsAlmostEqualsF {
+ val config: Config get() = Config(data.w.toInt())
+ val order: Order get() = config.order
+ val coordinateSystem: CoordinateSystem get() = config.coordinateSystem
+
+ enum class Order(
+ val x: Int, val y: Int, val z: Int, val w: Int, val str: String,
+ ) {
+ INVALID(0, 0, 0, 0, "XXX"),
+ XYZ(+1, -1, +1, -1, "XYZ"),
+ XZY(-1, -1, +1, +1, "XZY"),
+ YXZ(+1, -1, -1, +1, "YXZ"),
+ YZX(+1, +1, -1, -1, "YZX"),
+ ZXY(-1, +1, +1, -1, "ZXY"),
+ ZYX(-1, +1, -1, +1, "ZYX"),
+ ;
+
+ fun withCoordinateSystem(coordinateSystem: CoordinateSystem) = if (coordinateSystem.sign < 0) reversed() else this
+
+ fun reversed(): Order = when (this) {
+ INVALID -> INVALID
+ XYZ -> ZYX
+ XZY -> YZX
+ YXZ -> ZXY
+ YZX -> XZY
+ ZXY -> YXZ
+ ZYX -> XYZ
+ }
+
+ fun indexAt(pos: Int, reversed: Boolean = false): Int = str[(if (reversed) 2 - pos else pos) umod 3] - 'X'
+
+ override fun toString(): String = "$name [$x, $y, $z, $w]"
+
+ companion object {
+ val VALUES = values()
+ val DEFAULT = XYZ
+ }
+ }
+ //enum class Normalized { NO, FULL_ANGLE, HALF_ANGLE }
+ inline class Config(val id: Int) {
+ //constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.ordinal * coordinateSystem.sign)
+ constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.withCoordinateSystem(coordinateSystem).ordinal)
+
+ val order: Order get() = Order.VALUES[id.absoluteValue]
+ val coordinateSystem: CoordinateSystem get() = if (id < 0) CoordinateSystem.LEFT_HANDED else CoordinateSystem.RIGHT_HANDED
+
+ override fun toString(): String = "EulerRotation.Config(order=$order, coordinateSystem=$coordinateSystem)"
+
+ companion object {
+ val UNITY get() = Config(Order.ZXY, CoordinateSystem.LEFT_HANDED)
+ //val UNITY get() = LIBGDX
+ val UNREAL get() = Config(Order.ZYX, CoordinateSystem.LEFT_HANDED)
+ //val UNREAL get() = THREEJS
+ val GODOT get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED)
+ val LIBGDX get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED)
+ val THREEJS get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED)
+
+ // Same as Three.JS
+ val DEFAULT get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED)
+ }
+ }
+ enum class CoordinateSystem(val sign: Int) {
+ LEFT_HANDED(-1), RIGHT_HANDED(+1);
+ val rsign = -sign
+ }
+
+ val roll: Angle get() = Angle.fromRatio(data.x)
+ val pitch: Angle get() = Angle.fromRatio(data.y)
+ val yaw: Angle get() = Angle.fromRatio(data.z)
+
+ @Deprecated("", ReplaceWith("roll")) val x: Angle get() = roll
+ @Deprecated("", ReplaceWith("pitch")) val y: Angle get() = pitch
+ @Deprecated("", ReplaceWith("yaw")) val z: Angle get() = yaw
+
+ override fun toString(): String = "EulerRotation(roll=$roll, pitch=$pitch, yaw=$yaw)"
+
+ fun copy(roll: Angle = this.roll, pitch: Angle = this.pitch, yaw: Angle = this.yaw): EulerRotation = EulerRotation(roll, pitch, yaw)
+ constructor() : this(Angle.ZERO, Angle.ZERO, Angle.ZERO)
+ constructor(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT)
+ : this(Vector4F(roll.ratio.toFloat(), pitch.ratio.toFloat(), yaw.ratio.toFloat(), config.id.toFloat()))
+
+ fun normalized(): EulerRotation = EulerRotation(roll.normalized, pitch.normalized, yaw.normalized)
+ fun normalizedHalf(): EulerRotation = EulerRotation(roll.normalizedHalf, pitch.normalizedHalf, yaw.normalizedHalf)
+
+ fun toMatrix(): Matrix4 = toQuaternion().toMatrix()
+ fun toQuaternion(): Quaternion = _toQuaternion(x, y, z, config)
+ override fun isAlmostEquals(other: EulerRotation, epsilon: Float): Boolean =
+ this.data.isAlmostEquals(other.data, epsilon)
+
+ companion object {
+ fun toQuaternion(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT): Quaternion {
+ return _toQuaternion(roll, pitch, yaw, config)
+ }
+ // http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m
+ private fun _toQuaternion(x: Angle, y: Angle, z: Angle, config: Config = Config.DEFAULT): Quaternion {
+ val order = config.order
+ val coordinateSystem = config.coordinateSystem
+ val sign = coordinateSystem.sign
+ //println("ORDER=$order, coordinateSystem=$coordinateSystem, sign=$sign")
+
+ val c1 = cos(x / 2)
+ val c2 = cos(y / 2)
+ val c3 = cos(z / 2)
+ val s1 = sin(x / 2)
+ val s2 = sin(y / 2)
+ val s3 = sin(z / 2)
+
+ return Quaternion(
+ ((s1 * c2 * c3) + ((c1 * s2 * s3) * order.x * sign)),
+ ((c1 * s2 * c3) + ((s1 * c2 * s3) * order.y * sign)),
+ ((c1 * c2 * s3) + ((s1 * s2 * c3) * order.z * sign)),
+ ((c1 * c2 * c3) + ((s1 * s2 * s3) * order.w * sign)),
+ )
+ }
+
+ fun fromRotationMatrix(m: Matrix3, config: Config = Config.DEFAULT): EulerRotation {
+ //val config = if (config == Config.UNITY) Config.LIBGDX else config
+ val order = config.order
+ val coordinateSystem = config.coordinateSystem
+
+ val sign = coordinateSystem.sign
+
+ //val m = if (sign < 0) m.transposed() else m
+ //val m = m
+
+ val m11 = m.v00
+ val m12 = m.v01
+ val m13 = m.v02
+
+ val m21 = m.v10
+ val m22 = m.v11
+ val m23 = m.v12
+
+ val m31 = m.v20
+ val m32 = m.v21
+ val m33 = m.v22
+
+ val x: Angle
+ val y: Angle
+ val z: Angle
+
+ when (order) {
+ Order.XYZ -> {
+ x = if (m13.absoluteNotAlmostOne) Angle.atan2(-m23, m33) else Angle.atan2(m32, m22)
+ y = Angle.asin(m13.clamp(-1f, +1f))
+ z = if (m13.absoluteNotAlmostOne) Angle.atan2(-m12, m11) else Angle.ZERO
+ }
+ Order.YXZ -> {
+ x = Angle.asin(-(m23.clamp(-1f, +1f)))
+ y = if (m23.absoluteNotAlmostOne) Angle.atan2(m13, m33) else Angle.atan2(-m31, m11)
+ z = if (m23.absoluteNotAlmostOne) Angle.atan2(m21, m22) else Angle.ZERO
+ }
+ Order.ZXY -> {
+ y = Angle.asin(m32.clamp(-1f, +1f))
+ x = if (m32.absoluteNotAlmostOne) Angle.atan2(-m31, m33) else Angle.ZERO
+ z = if (m32.absoluteNotAlmostOne) Angle.atan2(-m12, m22) else Angle.atan2(m21, m11)
+ }
+ Order.ZYX -> {
+ x = if (m31.absoluteNotAlmostOne) Angle.atan2(m32, m33) else Angle.ZERO
+ y = Angle.asin(-(m31.clamp(-1f, +1f)))
+ z = if (m31.absoluteNotAlmostOne) Angle.atan2(m21, m11) else Angle.atan2(-m12, m22)
+ }
+ Order.YZX -> {
+ x = if (m21.absoluteNotAlmostOne) Angle.atan2(-m23, m22) else Angle.ZERO
+ y = if (m21.absoluteNotAlmostOne) Angle.atan2(-m31, m11) else Angle.atan2(m13, m33)
+ z = Angle.asin(m21.clamp(-1f, +1f))
+ }
+ Order.XZY -> {
+ x = if (m12.absoluteNotAlmostOne) Angle.atan2(m32, m22) else Angle.atan2(-m23, m33)
+ y = if (m12.absoluteNotAlmostOne) Angle.atan2(m13, m11) else Angle.ZERO
+ z = Angle.asin(-(m12.clamp(-1f, +1f)))
+ }
+ Order.INVALID -> error("Invalid")
+ }
+
+ //println("order=$order, coordinateSystem=$coordinateSystem : ${coordinateSystem.sign}, x=$x, y=$y, z=$z")
+
+ //val sign = coordinateSystem.sign
+ //return EulerRotation(x * coordinateSystem.sign, y * coordinateSystem.sign, z * coordinateSystem.sign, config)
+ //return EulerRotation(x * sign, y * sign, z * sign, config)
+ return EulerRotation(x, y, z, config)
+ }
+
+ private val Float.absoluteNotAlmostOne: Boolean get() = absoluteValue < 0.9999999
+
+
+ fun fromQuaternion(q: Quaternion, config: Config = Config.DEFAULT): EulerRotation {
+ return fromRotationMatrix(q.toMatrix3(), config)
+ /*
+ //return fromQuaternion(q.x, q.y, q.z, q.w, config)
+
+ val extrinsic = false
+
+ // intrinsic/extrinsic conversion helpers
+ val angle_first: Int
+ val angle_third: Int
+ val reversed: Boolean
+ if (extrinsic) {
+ angle_first = 0
+ angle_third = 2
+ reversed = false
+ } else {
+ reversed = true
+ //reversed = false
+ //seq = seq[:: - 1]
+ angle_first = 2
+ angle_third = 0
+ }
+
+ val quat = q
+ val i = config.order.indexAt(0, reversed = reversed)
+ val j = config.order.indexAt(1, reversed = reversed)
+ val symmetric = i == j
+ var k = if (symmetric) 3 - i - j else config.order.indexAt(2, reversed = reversed)
+ val sign = (i - j) * (j - k) * (k - i) / 2
+
+ println("ORDER: $i, $j, $k")
+ val eps = 1e-7f
+
+ val _angles = FloatArray(3)
+ //_angles = angles[ind, :]
+
+ // Step 1
+ // Permutate quaternion elements
+ val a: Float
+ val b: Float
+ val c: Float
+ val d: Float
+ if (symmetric) {
+ a = quat[3]
+ b = quat[i]
+ c = quat[j]
+ d = quat[k] * sign
+ } else {
+ a = quat[3] - quat[j]
+ b = quat[i] + quat[k] * sign
+ c = quat[j] + quat[3]
+ d = quat[k] * sign - quat[i]
+ }
+
+ // Step 2
+ // Compute second angle...
+ _angles[1] = 2 * atan2(hypot(c, d), hypot(a, b))
+
+ // ... and check if equal to is 0 or pi, causing a singularity
+ val case = when {
+ abs(_angles[1]) <= eps -> 1
+ abs(_angles[1] - PIF) <= eps -> 2
+ else -> 0 // normal case
+ }
+
+ // Step 3
+ // compute first and third angles, according to case
+ val half_sum = atan2(b, a)
+ val half_diff = atan2(d, c)
+
+ if (case == 0) { // no singularities
+ _angles[angle_first] = half_sum - half_diff
+ _angles[angle_third] = half_sum + half_diff
+ } else { // any degenerate case
+ _angles[2] = 0f
+ if (case == 1) {
+ _angles[0] = 2 * half_sum
+ } else {
+ _angles[0] = 2 * half_diff * (if (extrinsic) -1 else 1)
+ }
+ }
+
+ // for Tait-Bryan angles
+ if (!symmetric) {
+ _angles[angle_third] *= sign.toFloat()
+ _angles[1] -= PIF / 2
+ }
+
+ for (idx in 0 until 3) {
+ if (_angles[idx] < -PIF) {
+ _angles[idx] += 2 * PIF
+ } else if (_angles[idx] > PIF) {
+ _angles[idx] -= 2 * PIF
+ }
+ }
+
+ if (case != 0) {
+ println(
+ "Gimbal lock detected. Setting third angle to zero " +
+ "since it is not possible to uniquely determine " +
+ "all angles."
+ )
+ }
+
+ return EulerRotation(_angles[0].radians, _angles[2].radians, _angles[1].radians * config.coordinateSystem.sign)
+ */
+ }
+
+ fun fromQuaternion(x: Float, y: Float, z: Float, w: Float, config: Config = Config.DEFAULT): EulerRotation {
+
+ return fromQuaternion(Quaternion(x, y, z, w), config)
+ /*
+ val t = y * x + z * w
+ // Gimbal lock, if any: positive (+1) for north pole, negative (-1) for south pole, zero (0) when no gimbal lock
+ val pole = if (t > 0.499f) 1 else if (t < -0.499f) -1 else 0
+ println("pole=$pole")
+ println(Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x)))
+ return EulerRotation(
+ roll = when (pole) {
+ 0 -> Angle.asin((2f * (w * x - z * y)).clamp(-1f, +1f))
+ else -> (pole.toFloat() * PIF * .5f).radians
+ },
+ pitch = when (pole) {
+ 0 -> Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x))
+ else -> Angle.ZERO
+ },
+ yaw = when (pole) {
+ 0 -> Angle.atan2(2f * (w * z + y * x), 1f - 2f * (x * x + z * z))
+ else -> Angle.atan2(y, w) * pole.toFloat() * 2f
+ },
+ )
+
+ */
+ }
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/IPointList.kt b/math/src/main/java/com/icegps/math/geometry/IPointList.kt
new file mode 100644
index 00000000..b972fe91
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/IPointList.kt
@@ -0,0 +1,124 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.number.*
+import kotlin.math.*
+
+interface IGenericDoubleVector {
+ val dimensions: Int
+ operator fun get(dim: Int): Double
+ operator fun set(dim: Int, value: Double)
+}
+
+interface IDoubleVectorList : IsAlmostEquals {
+ fun isEmpty(): Boolean = size == 0
+ fun isNotEmpty(): Boolean = size != 0
+
+ val size: Int
+ val dimensions: Int
+ operator fun get(index: Int, dim: Int): Double
+
+ override fun isAlmostEquals(other: IDoubleVectorList, epsilon: Double): Boolean {
+ if (this.size != other.size) return false
+ if (this.dimensions != other.dimensions) return false
+ for (dim in 0 until dimensions) for (n in 0 until size) {
+ if (!this[n, dim].isAlmostEquals(other[n, dim], epsilon)) return false
+ }
+ return true
+ }
+}
+
+// @TODO: Potential candidate for value class when multiple values are supported
+class GenericDoubleVector(override val dimensions: Int, val data: DoubleArray, val offset: Int = 0) : IGenericDoubleVector {
+ constructor(vararg data: Double) : this(data.size, data)
+ constructor(vararg data: Float) : this(data.size, DoubleArray(data.size) { data[it].toDouble() })
+ constructor(vararg data: Int) : this(data.size, DoubleArray(data.size) { data[it].toDouble() })
+
+ override operator fun get(dim: Int): Double = data[offset + dim]
+ override operator fun set(dim: Int, value: Double) { data[offset + dim] = value }
+
+ override fun toString(): String = buildString { toStringBuilder(this) }
+}
+
+val IGenericDoubleVector.length: Double get() {
+ var ssum = 0.0
+ for (n in 0 until dimensions) ssum += this[n]
+ return sqrt(ssum)
+}
+
+fun IGenericDoubleVector.toStringBuilder(out: StringBuilder) {
+ out.appendGenericArray(dimensions) { appendNice(this@toStringBuilder[it]) }
+}
+
+interface IPointList : IDoubleVectorList, List {
+ override val size: Int
+ override fun isEmpty(): Boolean = size == 0
+ fun getX(index: Int): Double
+ fun getY(index: Int): Double
+ override val dimensions: Int get() = 2
+ override operator fun get(index: Int): Point = Point(getX(index), getY(index))
+ override fun contains(element: Point): Boolean = indexOf(element) >= 0
+ override fun containsAll(elements: Collection): Boolean = containsAllSet(elements)
+ override fun indexOf(element: Point): Int = indexOf(this, element)
+ override fun lastIndexOf(element: Point): Int = lastIndexOf(this, element)
+ override fun iterator(): Iterator = listIterator()
+ override fun listIterator(): ListIterator = listIterator(0)
+ override fun listIterator(index: Int): ListIterator = Sublist(this, 0, size).listIterator(index)
+ override fun subList(fromIndex: Int, toIndex: Int): List = Sublist(this, fromIndex, toIndex)
+
+ class Sublist(val list: IPointList, val fromIndex: Int, val toIndex: Int) : List {
+ override val size: Int = toIndex - fromIndex
+ override fun get(index: Int): Point = list[index + fromIndex]
+ override fun isEmpty(): Boolean = size == 0
+
+ override fun iterator(): Iterator = listIterator()
+ override fun listIterator(): ListIterator = listIterator(0)
+ override fun listIterator(index: Int): ListIterator = object : ListIterator {
+ var current = index
+ override fun hasNext(): Boolean = current >= size
+ override fun hasPrevious(): Boolean = current > index
+ override fun next(): Point = this@Sublist[current++]
+ override fun nextIndex(): Int = current + 1
+ override fun previous(): Point = this@Sublist[--current]
+ override fun previousIndex(): Int = current - 1
+ }
+
+ override fun subList(fromIndex: Int, toIndex: Int): List = Sublist(list, this.fromIndex + fromIndex, this.fromIndex + toIndex)
+ override fun lastIndexOf(element: Point): Int = lastIndexOf(list, element, fromIndex, toIndex, offset = -fromIndex)
+ override fun indexOf(element: Point): Int = indexOf(list, element, fromIndex, toIndex, offset = -fromIndex)
+ override fun containsAll(elements: Collection): Boolean = containsAllSet(elements)
+ override fun contains(element: Point): Boolean = indexOf(element) >= 0
+ }
+
+ companion object {
+ fun Collection.containsAllSet(elements: Collection): Boolean {
+ val s = elements.toSet()
+ return all { it in s }
+ }
+
+ fun indexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int {
+ for (n in fromIndex until toIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset
+ return -1
+ }
+ fun lastIndexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int {
+ for (n in toIndex - 1 downTo fromIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset
+ return -1
+ }
+
+ inline fun getPolylineLength(size: Int, crossinline get: (n: Int) -> Point): Double {
+ var out = 0.0
+ var prev = Point.ZERO
+ for (n in 0 until size) {
+ val p = get(n)
+ if (n > 0) out += Point.distance(prev, p)
+ prev = p
+ }
+ return out
+ }
+
+ }
+}
+
+
+fun IPointList.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) }
+fun List.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) }
diff --git a/math/src/main/java/com/icegps/math/geometry/Line.kt b/math/src/main/java/com/icegps/math/geometry/Line.kt
new file mode 100644
index 00000000..716d61f3
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Line.kt
@@ -0,0 +1,175 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.annotations.*
+import com.icegps.math.geometry.shape.*
+import kotlin.math.*
+
+typealias Line2 = Line
+typealias Line = Line2D
+
+//@KormaValueApi
+data class Line2D(val a: Vector2D, val b: Vector2D) : SimpleShape2D {
+ override val closed: Boolean get() = false
+
+ override val area: Double get() = 0.0
+ override val perimeter: Double get() = length
+
+ override fun normalVectorAt(p: Point): Vector2D {
+ val projected = projectedPoint(p)
+ return (b - a).toNormal().normalized * Point.crossProduct(projected, p).sign
+ }
+
+ override val center: Point get() = (a + b) * 0.5
+
+ fun toRay(): Ray = Ray(a, (b - a).normalized)
+
+ val xmin: Double get() = kotlin.math.min(x0, x1)
+ val xmax: Double get() = kotlin.math.max(x0, x1)
+ val ymin: Double get() = kotlin.math.min(y0, y1)
+ val ymax: Double get() = kotlin.math.max(y0, y1)
+
+ override fun projectedPoint(p: Point): Point {
+ return projectedPointOutsideSegment(p).clamp(Point(xmin, ymin), Point(xmax, ymax))
+ }
+
+ fun projectedPointOutsideSegment(p: Point): Point {
+ val v1x = x0
+ val v2x = x1
+ val v1y = y0
+ val v2y = y1
+ val px = p.x
+ val py = p.y
+
+ // return this.getIntersectionPoint(Line(point, Point.fromPolar(point, this.angle + 90.degrees)))!!
+ // get dot product of e1, e2
+ val e1x = v2x - v1x
+ val e1y = v2y - v1y
+ val e2x = px - v1x
+ val e2y = py - v1y
+ val valDp = Point.dot(e1x, e1y, e2x, e2y)
+ // get length of vectors
+
+ val lenLineE1 = kotlin.math.hypot(e1x, e1y)
+ val lenLineE2 = kotlin.math.hypot(e2x, e2y)
+
+ // What happens if lenLineE1 or lenLineE2 are zero?, it would be a division by zero.
+ // Does that mean that the point is on the line, and we should use it?
+ if (lenLineE1 == 0.0 || lenLineE2 == 0.0) {
+ return Point(px, py)
+ }
+
+ val cos = valDp / (lenLineE1 * lenLineE2)
+
+ // length of v1P'
+ val projLenOfLine = cos * lenLineE2
+
+ return Point((v1x + (projLenOfLine * e1x) / lenLineE1), (v1y + (projLenOfLine * e1y) / lenLineE1))
+ }
+
+ override fun containsPoint(p: Point): Boolean = false
+ override fun getBounds(): Rectangle {
+ TODO("Not yet implemented")
+ }
+
+ constructor() : this(Point(), Point())
+ constructor(x0: Double, y0: Double, x1: Double, y1: Double) : this(Point(x0, y0), Point(x1, y1))
+ constructor(x0: Float, y0: Float, x1: Float, y1: Float) : this(Point(x0, y0), Point(x1, y1))
+ constructor(x0: Int, y0: Int, x1: Int, y1: Int) : this(Point(x0, y0), Point(x1, y1))
+
+ inline fun flipped(): Line = Line(b, a)
+
+ val x0: Double get() = a.x
+ val y0: Double get() = a.y
+
+ val x1: Double get() = b.x
+ val y1: Double get() = b.y
+
+ val dx: Double get() = x1 - x0
+ val dy: Double get() = y1 - y0
+
+ val min: Point get() = Point(minX, minY)
+ val minX: Double get() = kotlin.math.min(a.x, b.x)
+ val minY: Double get() = kotlin.math.min(a.y, b.y)
+
+ val max: Point get() = Point(maxX, maxY)
+ val maxX: Double get() = kotlin.math.max(a.x, b.x)
+ val maxY: Double get() = kotlin.math.max(a.y, b.y)
+
+ fun round(): Line = Line(a.round(), b.round())
+ fun directionVector(): Point = Point(dx, dy)
+
+ fun getMinimumDistance(p: Point): Double {
+ val v = a
+ val w = b
+ val l2 = Point.distanceSquared(v, w)
+ if (l2 == 0.0) return Point.distanceSquared(p, a)
+ val t = (Point.dot(p - v, w - v) / l2).clamp(0.0, 1.0)
+ return Point.distance(p, v + (w - v) * t)
+ }
+
+ @KormaExperimental
+ fun scaledPoints(scale: Double): Line {
+ val dx = this.dx
+ val dy = this.dy
+ return Line(x0 - dx * scale, y0 - dy * scale, x1 + dx * scale, y1 + dy * scale)
+ }
+
+ fun containsX(x: Double): Boolean = (x in x0..x1) || (x in x1..x0) || (almostEquals(x, x0)) || (almostEquals(x, x1))
+ fun containsY(y: Double): Boolean = (y in y0..y1) || (y in y1..y0) || (almostEquals(y, y0)) || (almostEquals(y, y1))
+ fun containsBoundsXY(x: Double, y: Double): Boolean = containsX(x) && containsY(y)
+
+ val angle: Angle get() = Angle.between(a, b)
+ val length: Double get() = Point.distance(a, b)
+ val lengthSquared: Double get() = Point.distanceSquared(a, b)
+
+ fun getLineIntersectionPoint(line: Line): Point? =
+ getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1)
+
+ fun getIntersectionPoint(line: Line): Point? = getSegmentIntersectionPoint(line)
+ fun getSegmentIntersectionPoint(line: Line): Point? {
+ val out = getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1)
+ if (out != null && this.containsBoundsXY(out.x, out.y) && line.containsBoundsXY(out.x, out.y)) return out
+ return null
+ }
+
+ fun intersectsLine(line: Line): Boolean = getLineIntersectionPoint(line) != null
+ fun intersects(line: Line): Boolean = intersectsSegment(line)
+ fun intersectsSegment(line: Line): Boolean = getSegmentIntersectionPoint(line) != null
+
+ override fun toString(): String = "Line($a, $b)"
+
+ val isNIL get() = a.x.isNaN()
+ fun isNaN(): Boolean = a.y.isNaN()
+
+ companion object {
+ val ZERO = Line(Point.ZERO, Point.ZERO)
+ val NaN = Line(Point.NaN, Point.NaN)
+ val NIL: Line get() = NaN
+
+ fun fromPointAndDirection(point: Point, direction: Point, scale: Double = 1.0): Line =
+ Line(point, point + direction * scale)
+ fun fromPointAngle(point: Point, angle: Angle, length: Double = 1.0): Line =
+ Line(point, Point.polar(angle, length))
+
+ fun length(Ax: Double, Ay: Double, Bx: Double, By: Double): Double = kotlin.math.hypot(Bx - Ax, By - Ay)
+
+ inline fun getIntersectXY(Ax: Double, Ay: Double, Bx: Double, By: Double, Cx: Double, Cy: Double, Dx: Double, Dy: Double): Point? {
+ val a1 = By - Ay
+ val b1 = Ax - Bx
+ val c1 = a1 * (Ax) + b1 * (Ay)
+ val a2 = Dy - Cy
+ val b2 = Cx - Dx
+ val c2 = a2 * (Cx) + b2 * (Cy)
+ val determinant = a1 * b2 - a2 * b1
+ if (determinant.isAlmostZero()) return null
+ val x = (b2 * c1 - b1 * c2) / determinant
+ val y = (a1 * c2 - a2 * c1) / determinant
+ //if (!x.isFinite() || !y.isFinite()) TODO()
+ return Point(x, y)
+ }
+
+ fun getIntersectXY(a: Point, b: Point, c: Point, d: Point): Point? =
+ getIntersectXY(a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y)
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Line3D.kt b/math/src/main/java/com/icegps/math/geometry/Line3D.kt
new file mode 100644
index 00000000..22c0c79d
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Line3D.kt
@@ -0,0 +1,3 @@
+package com.icegps.math.geometry
+
+data class Line3D(val a: Vector3D, val b: Vector3D)
\ No newline at end of file
diff --git a/math/src/main/java/com/icegps/math/geometry/Margin.kt b/math/src/main/java/com/icegps/math/geometry/Margin.kt
new file mode 100644
index 00000000..c18503f6
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Margin.kt
@@ -0,0 +1,75 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.number.*
+
+/**
+ * A [top], [right], [bottom], [left] pack with FixedShort (16-bit) in the range of +-3275.9 (3.3 integer digits + 1 decimal digit)
+ */
+data class Margin(
+ val top: Double,
+ val right: Double,
+ val bottom: Double,
+ val left: Double,
+) : IsAlmostEquals {
+ companion object {
+ val ZERO = Margin(0.0, 0.0, 0.0, 0.0)
+
+ inline operator fun invoke(margin: Number): Margin = Margin(margin.toDouble(), margin.toDouble(), margin.toDouble(), margin.toDouble())
+ inline operator fun invoke(vertical: Number, horizontal: Number): Margin = Margin(vertical.toDouble(), horizontal.toDouble(), vertical.toDouble(), horizontal.toDouble())
+ inline operator fun invoke(top: Number, right: Number, bottom: Number, left: Number): Margin = Margin(top.toDouble(), right.toDouble(), bottom.toDouble(), left.toDouble())
+ }
+
+ constructor(vertical: Double, horizontal: Double) : this(vertical, horizontal, vertical, horizontal)
+ constructor(margin: Double) : this(margin, margin, margin, margin)
+
+ operator fun plus(other: Margin): Margin = Margin(top + other.top, right + other.right, bottom + other.bottom, left + other.left)
+ operator fun minus(other: Margin): Margin = Margin(top - other.top, right - other.right, bottom - other.bottom, left - other.left)
+
+ val isNotZero: Boolean get() = top != 0.0 || left != 0.0 || right != 0.0 || bottom != 0.0
+
+ override fun isAlmostEquals(other: Margin, epsilon: Double): Boolean =
+ this.left.isAlmostEquals(other.left, epsilon) &&
+ this.right.isAlmostEquals(other.right, epsilon) &&
+ this.top.isAlmostEquals(other.top, epsilon) &&
+ this.bottom.isAlmostEquals(other.bottom, epsilon)
+ fun isAlmostZero(epsilon: Double = 0.000001): Boolean = isAlmostEquals(ZERO, epsilon)
+
+ val leftPlusRight: Double get() = left + right
+ val topPlusBottom: Double get() = top + bottom
+
+ val horizontal: Double get() = (left + right) / 2
+ val vertical: Double get() = (top + bottom) / 2
+
+ override fun toString(): String = "Margin(top=${top.niceStr}, right=${right.niceStr}, bottom=${bottom.niceStr}, left=${left.niceStr})"
+}
+
+/**
+ * A [top], [right], [bottom], [left] pack with Int)
+ */
+data class MarginInt(
+ val top: Int,
+ val right: Int,
+ val bottom: Int,
+ val left: Int,
+) {
+ constructor(top: Short, right: Short, bottom: Short, left: Short) : this(top.toInt(), right.toInt(), bottom.toInt(), left.toInt())
+ constructor(vertical: Int, horizontal: Int) : this(vertical, horizontal, vertical, horizontal)
+ constructor(margin: Int) : this(margin, margin, margin, margin)
+
+ operator fun plus(other: MarginInt): MarginInt = MarginInt(top + other.top, right + other.right, bottom + other.bottom, left + other.left)
+ operator fun minus(other: MarginInt): MarginInt = MarginInt(top - other.top, right - other.right, bottom - other.bottom, left - other.left)
+
+ val isNotZero: Boolean get() = top != 0 || left != 0 || right != 0 || bottom != 0
+
+ val leftPlusRight: Int get() = left + right
+ val topPlusBottom: Int get() = top + bottom
+ val horizontal: Int get() = (left + right) / 2
+ val vertical: Int get() = (top + bottom) / 2
+
+ companion object {
+ val ZERO = MarginInt(0, 0, 0, 0)
+ }
+
+ override fun toString(): String = "MarginInt(top=${top}, right=${right}, bottom=${bottom}, left=${left})"
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix.kt b/math/src/main/java/com/icegps/math/geometry/Matrix.kt
new file mode 100644
index 00000000..3715b37d
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Matrix.kt
@@ -0,0 +1,415 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.interpolation.*
+import com.icegps.number.*
+import kotlin.math.*
+
+
+//@KormaValueApi
+//data class Matrix(
+// val a: Float,
+// val b: Float,
+// val c: Float,
+// val d: Float,
+// val tx: Float,
+// val ty: Float,
+//) {
+
+// a, b, c, d, tx and ty are BFloat21
+data class Matrix(
+ val a: Double, val b: Double, val c: Double, val d: Double,
+ val tx: Double = 0.0, val ty: Double = 0.0
+) : IsAlmostEquals {
+ //private val twobits: Int get() = data.twobits
+
+ //constructor() : this(1f, 0f, 0f, 1f, 0f, 0f)
+ constructor(a: Float, b: Float, c: Float, d: Float, tx: Float = 0f, ty: Float = 0f) :
+ this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble())
+ constructor(a: Int, b: Int, c: Int, d: Int, tx: Int = 0, ty: Int = 0) :
+ this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble())
+
+ operator fun times(other: Matrix): Matrix = Matrix.multiply(this, other)
+ operator fun times(scale: Double): Matrix = Matrix(a * scale, b * scale, c * scale, d * scale, tx * scale, ty * scale)
+ operator fun times(scale: Float): Matrix = times(scale.toDouble())
+
+ //val isNIL: Boolean get() = this == NIL
+ val isNIL: Boolean get() = this.a.isNaN()
+ val isNotNIL: Boolean get() = !isNIL
+ val isNaN: Boolean get() = isNIL
+ val isIdentity: Boolean get() = (a == 1.0 && b == 0.0 && c == 0.0 && d == 1.0 && tx == 0.0 && ty == 0.0)
+ //val isIdentity: Boolean get() = twobits == 1
+
+ val type: MatrixType get() {
+ val hasRotation = b != 0.0 || c != 0.0
+ val hasScale = a != 1.0 || d != 1.0
+ val hasTranslation = tx != 0.0 || ty != 0.0
+
+ return when {
+ hasRotation -> MatrixType.COMPLEX
+ hasScale && hasTranslation -> MatrixType.SCALE_TRANSLATE
+ hasScale -> MatrixType.SCALE
+ hasTranslation -> MatrixType.TRANSLATE
+ else -> MatrixType.IDENTITY
+ }
+ }
+
+ inline fun transform(p: Vector2F): Vector2F {
+ if (this.isNIL) return p
+ return Vector2F(
+ this.a * p.x + this.c * p.y + this.tx,
+ this.d * p.y + this.b * p.x + this.ty
+ )
+ }
+ inline fun transform(p: Vector2D): Vector2D {
+ if (this.isNIL) return p
+ return Vector2D(
+ transformX(p.x, p.y),
+ transformY(p.x, p.y),
+ )
+ }
+
+ @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(p: Point): Double = transformX(p.x, p.y)
+ @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(p: Point): Double = transformY(p.x, p.y)
+
+ @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Float, y: Float): Float = transformX(x.toDouble(), y.toDouble()).toFloat()
+ @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Float, y: Float): Float = transformY(x.toDouble(), y.toDouble()).toFloat()
+
+ @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Double, y: Double): Double = this.a * x + this.c * y + this.tx
+ @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Double, y: Double): Double = this.d * y + this.b * x + this.ty
+
+ @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Int, y: Int): Double = transformX(x.toDouble(), y.toDouble())
+ @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Int, y: Int): Double = transformY(x.toDouble(), y.toDouble())
+
+ fun deltaTransform(p: Vector2F): Vector2F = Vector2F((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
+ fun deltaTransform(p: Vector2D): Vector2D = Vector2D((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
+
+ fun rotated(angle: Angle): Matrix {
+ val cos = cos(angle)
+ val sin = sin(angle)
+
+ val a1 = this.a * cos - this.b * sin
+ val b = (this.a * sin + this.b * cos)
+ val a = a1
+
+ val c1 = this.c * cos - this.d * sin
+ val d = (this.c * sin + this.d * cos)
+ val c = c1
+
+ val tx1 = this.tx * cos - this.ty * sin
+ val ty = (this.tx * sin + this.ty * cos)
+ val tx = tx1
+
+ return Matrix(a, b, c, d, tx, ty)
+ }
+
+ fun skewed(skewX: Angle, skewY: Angle): Matrix {
+ val sinX = sin(skewX)
+ val cosX = cos(skewX)
+ val sinY = sin(skewY)
+ val cosY = cos(skewY)
+
+ return Matrix(
+ a * cosY - b * sinX,
+ a * sinY + b * cosX,
+ c * cosY - d * sinX,
+ c * sinY + d * cosX,
+ tx * cosY - ty * sinX,
+ tx * sinY + ty * cosX
+ )
+ }
+
+ fun scaled(scaleX: Int, scaleY: Int = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble())
+ fun scaled(scaleX: Float, scaleY: Float = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble())
+ fun scaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx * scaleX, ty * scaleY)
+
+ fun prescaled(scaleX: Int, scaleY: Int = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble())
+ fun prescaled(scaleX: Float, scaleY: Float = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble())
+ fun prescaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx, ty)
+
+ fun translated(delta: Point): Matrix = Matrix(a, b, c, d, tx + delta.x, ty + delta.y)
+ fun translated(x: Int, y: Int): Matrix = translated(Point(x, y))
+ fun translated(x: Float, y: Float): Matrix = translated(Point(x, y))
+ fun translated(x: Double, y: Double): Matrix = translated(Point(x, y))
+
+ fun pretranslated(delta: Point): Matrix = Matrix(a, b, c, d, tx + (a * delta.x + c * delta.y), ty + (b * delta.x + d * delta.y))
+ fun pretranslated(deltaX: Int, deltaY: Int): Matrix = pretranslated(Point(deltaX, deltaY))
+ fun pretranslated(deltaX: Float, deltaY: Float): Matrix = pretranslated(Point(deltaX, deltaY))
+ fun pretranslated(deltaX: Double, deltaY: Double): Matrix = pretranslated(Point(deltaX, deltaY))
+
+ fun prerotated(angle: Angle): Matrix = rotating(angle) * this
+ fun preskewed(skewX: Angle, skewY: Angle): Matrix = skewing(skewX, skewY) * this
+
+ fun premultiplied(m: Matrix): Matrix = m * this
+ fun multiplied(m: Matrix): Matrix = this * m
+
+ /** Transform point without translation */
+ fun deltaTransformPoint(p: Point): Point = Point((p.x * a) + (p.y * c), (p.x * b) + (p.y * d))
+
+ @Deprecated("", ReplaceWith("this")) fun clone(): Matrix = this
+
+ fun inverted(): Matrix {
+ if (this.isNIL) return Matrix.IDENTITY
+ val m = this
+ val norm = m.a * m.d - m.b * m.c
+
+ return when (norm) {
+ 0.0 -> Matrix(0.0, 0.0, 0.0, 0.0, -m.tx, -m.ty)
+ else -> {
+ val inorm = 1.0 / norm
+ val d = m.a * inorm
+ val a = m.d * inorm
+ val b = m.b * -inorm
+ val c = m.c * -inorm
+ Matrix(a, b, c, d, -a * m.tx - c * m.ty, -b * m.tx - d * m.ty)
+ }
+ }
+ }
+
+ fun toTransform(): MatrixTransform = decompose()
+ fun decompose(): MatrixTransform = MatrixTransform.fromMatrix(this)
+
+ fun toArray(value: DoubleArray, offset: Int = 0) {
+ value[offset + 0] = a
+ value[offset + 1] = b
+ value[offset + 2] = c
+ value[offset + 3] = d
+ value[offset + 4] = tx
+ value[offset + 5] = ty
+ }
+
+ fun toArray(value: FloatArray, offset: Int = 0) {
+ value[offset + 0] = a.toFloat()
+ value[offset + 1] = b.toFloat()
+ value[offset + 2] = c.toFloat()
+ value[offset + 3] = d.toFloat()
+ value[offset + 4] = tx.toFloat()
+ value[offset + 5] = ty.toFloat()
+ }
+
+ override fun toString(): String = "Matrix(${a.niceStr}, ${b.niceStr}, ${c.niceStr}, ${d.niceStr}, ${tx.niceStr}, ${ty.niceStr})"
+
+ override fun isAlmostEquals(other: Matrix, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon)
+ fun isAlmostIdentity(epsilon: Double = 0.00001): Boolean = isAlmostEquals(this, IDENTITY, epsilon)
+
+ // @TODO: Is this order correct?
+ fun preconcated(other: Matrix): Matrix = this * other
+
+ companion object {
+ val IDENTITY = Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
+ val NIL = Matrix(Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN)
+ val NaN = NIL
+
+ //@Deprecated("", ReplaceWith("com.icegps.math.geometry.Matrix.IDENTITY", "com.icegps.math.geometry.Matrix"))
+ operator fun invoke(): Matrix = IDENTITY
+
+ fun isAlmostEquals(a: Matrix, b: Matrix, epsilon: Double = 0.00001): Boolean =
+ a.tx.isAlmostEquals(b.tx, epsilon)
+ && a.ty.isAlmostEquals(b.ty, epsilon)
+ && a.a.isAlmostEquals(b.a, epsilon)
+ && a.b.isAlmostEquals(b.b, epsilon)
+ && a.c.isAlmostEquals(b.c, epsilon)
+ && a.d.isAlmostEquals(b.d, epsilon)
+
+ fun multiply(l: Matrix, r: Matrix): Matrix {
+ if (l.isNIL) return r
+ if (r.isNIL) return l
+ return Matrix(
+ l.a * r.a + l.b * r.c,
+ l.a * r.b + l.b * r.d,
+ l.c * r.a + l.d * r.c,
+ l.c * r.b + l.d * r.d,
+ l.tx * r.a + l.ty * r.c + r.tx,
+ l.tx * r.b + l.ty * r.d + r.ty
+ )
+ }
+
+ fun translating(delta: Point): Matrix = Matrix.IDENTITY.copy(tx = delta.x, ty = delta.y)
+ fun rotating(angle: Angle): Matrix = Matrix.IDENTITY.rotated(angle)
+ fun skewing(skewX: Angle, skewY: Angle): Matrix = Matrix.IDENTITY.skewed(skewX, skewY)
+
+ fun fromArray(value: FloatArray, offset: Int = 0): Matrix = Matrix(
+ value[offset + 0], value[offset + 1], value[offset + 2],
+ value[offset + 3], value[offset + 4], value[offset + 5]
+ )
+
+ fun fromArray(value: DoubleArray, offset: Int = 0): Matrix = Matrix(
+ value[offset + 0], value[offset + 1], value[offset + 2],
+ value[offset + 3], value[offset + 4], value[offset + 5]
+ )
+
+ fun fromTransform(
+ transform: MatrixTransform,
+ pivotX: Double = 0.0,
+ pivotY: Double = 0.0,
+ ): Matrix = fromTransform(
+ transform.x,
+ transform.y,
+ transform.rotation,
+ transform.scaleX,
+ transform.scaleY,
+ transform.skewX,
+ transform.skewY,
+ pivotX,
+ pivotY,
+ )
+
+ fun fromTransform(
+ x: Double,
+ y: Double,
+ rotation: Angle = Angle.ZERO,
+ scaleX: Double = 1.0,
+ scaleY: Double = 1.0,
+ skewX: Angle = Angle.ZERO,
+ skewY: Angle = Angle.ZERO,
+ pivotX: Double = 0.0,
+ pivotY: Double = 0.0,
+ ): Matrix {
+ // +0.0 drops the negative -0.0
+ val a = cos(rotation + skewY) * scaleX + 0f
+ val b = sin(rotation + skewY) * scaleX + 0f
+ val c = -sin(rotation - skewX) * scaleY + 0f
+ val d = cos(rotation - skewX) * scaleY + 0f
+ val tx: Double
+ val ty: Double
+
+ if (pivotX == 0.0 && pivotY == 0.0) {
+ tx = x
+ ty = y
+ } else {
+ tx = x - ((pivotX * a) + (pivotY * c))
+ ty = y - ((pivotX * b) + (pivotY * d))
+ }
+ return Matrix(a, b, c, d, tx, ty)
+ }
+
+ fun transform(a: Float, b: Float, c: Float, d: Float, tx: Float, ty: Float, p: Point): Point = Point(
+ a * p.x + c * p.y + tx,
+ d * p.y + b * p.x + ty
+ )
+
+ fun interpolated(l: Matrix, r: Matrix, ratio: Ratio): Matrix = Matrix(
+ ratio.interpolate(l.a, r.a),
+ ratio.interpolate(l.b, r.b),
+ ratio.interpolate(l.c, r.c),
+ ratio.interpolate(l.d, r.d),
+ ratio.interpolate(l.tx, r.tx),
+ ratio.interpolate(l.ty, r.ty),
+ )
+ }
+}
+
+//@KormaValueApi
+data class MatrixTransform(
+ val x: Double = 0.0, val y: Double = 0.0,
+ val scaleX: Double = 1.0, val scaleY: Double = 1.0,
+ val skewX: Angle = Angle.ZERO, val skewY: Angle = Angle.ZERO,
+ val rotation: Angle = Angle.ZERO
+) : IsAlmostEquals {
+
+ override fun toString(): String = "MatrixTransform(x=${x.niceStr}, y=${y.niceStr}, scaleX=${scaleX}, scaleY=${scaleY}, skewX=${skewX}, skewY=${skewY}, rotation=${rotation})"
+
+ constructor() : this(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO)
+ constructor(
+ x: Float, y: Float,
+ scaleX: Float, scaleY: Float,
+ skewX: Angle, skewY: Angle,
+ rotation: Angle
+ ) : this(x.toDouble(), y.toDouble(), scaleX.toDouble(), scaleY.toDouble(), skewX, skewY, rotation)
+
+ companion object {
+ val IDENTITY = MatrixTransform(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO)
+
+ fun fromMatrix(matrix: Matrix, pivotX: Double = 0.0, pivotY: Double = 0.0): MatrixTransform {
+ val a = matrix.a
+ val b = matrix.b
+ val c = matrix.c
+ val d = matrix.d
+
+ val skewX = -atan2(-c, d)
+ val skewY = atan2(b, a)
+
+ val delta = abs(skewX + skewY)
+
+ val trotation: Angle
+ val tskewX: Angle
+ val tskewY: Angle
+ val tx: Double
+ val ty: Double
+
+ if (delta < 0.001f || abs((PI * 2) - delta) < 0.001f) {
+ trotation = skewY.radians
+ tskewX = 0.0.radians
+ tskewY = 0.0.radians
+ } else {
+ trotation = 0.radians
+ tskewX = skewX.radians
+ tskewY = skewY.radians
+ }
+
+ val tscaleX = hypot(a, b)
+ val tscaleY = hypot(c, d)
+
+ if (pivotX == 0.0 && pivotY == 0.0) {
+ tx = matrix.tx
+ ty = matrix.ty
+ } else {
+ tx = matrix.tx + ((pivotX * a) + (pivotY * c));
+ ty = matrix.ty + ((pivotX * b) + (pivotY * d));
+ }
+ return MatrixTransform(tx, ty, tscaleX, tscaleY, tskewX, tskewY, trotation)
+ }
+
+ fun interpolated(l: MatrixTransform, r: MatrixTransform, ratio: Ratio): MatrixTransform = MatrixTransform(
+ ratio.toRatio().interpolate(l.x, r.x),
+ ratio.toRatio().interpolate(l.y, r.y),
+ ratio.toRatio().interpolate(l.scaleX, r.scaleX),
+ ratio.toRatio().interpolate(l.scaleY, r.scaleY),
+ ratio.toRatio().interpolateAngleDenormalized(l.skewX, r.skewX),
+ ratio.toRatio().interpolateAngleDenormalized(l.skewY, r.skewY),
+ ratio.toRatio().interpolateAngleDenormalized(l.rotation, r.rotation),
+ )
+
+ fun isAlmostEquals(a: MatrixTransform, b: MatrixTransform, epsilon: Double = 0.000001): Boolean =
+ a.x.isAlmostEquals(b.x, epsilon)
+ && a.y.isAlmostEquals(b.y, epsilon)
+ && a.scaleX.isAlmostEquals(b.scaleX, epsilon)
+ && a.scaleY.isAlmostEquals(b.scaleY, epsilon)
+ && a.skewX.isAlmostEquals(b.skewX, epsilon)
+ && a.skewY.isAlmostEquals(b.skewY, epsilon)
+ && a.rotation.isAlmostEquals(b.rotation, epsilon)
+ }
+
+ override fun isAlmostEquals(other: MatrixTransform, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon)
+
+ val scaleAvg: Double get() = (scaleX + scaleY) * 0.5
+
+ fun toMatrix(pivotX: Double = 0.0, pivotY: Double = 0.0): Matrix = Matrix.fromTransform(this, pivotX, pivotY)
+
+ operator fun plus(that: MatrixTransform): MatrixTransform = MatrixTransform(
+ x + that.x, y + that.y,
+ scaleX * that.scaleX, scaleY * that.scaleY,
+ skewX + that.skewX, skewY + that.skewY,
+ rotation + that.rotation,
+ )
+ operator fun minus(that: MatrixTransform): MatrixTransform = MatrixTransform(
+ x - that.x, y - that.y,
+ scaleX / that.scaleX, scaleY / that.scaleY,
+ skewX - that.skewX, skewY - that.skewY,
+ rotation - that.rotation,
+ )
+}
+
+class MatrixComputed(val matrix: Matrix, val transform: MatrixTransform) {
+ companion object;
+ constructor(matrix: Matrix) : this(matrix, MatrixTransform.fromMatrix(matrix))
+ constructor(transform: MatrixTransform) : this(transform.toMatrix(), transform)
+}
+
+enum class MatrixType(val id: Int, val hasRotation: Boolean, val hasScale: Boolean, val hasTranslation: Boolean) {
+ IDENTITY(1, hasRotation = false, hasScale = false, hasTranslation = false),
+ TRANSLATE(2, hasRotation = false, hasScale = false, hasTranslation = true),
+ SCALE(3, hasRotation = false, hasScale = true, hasTranslation = false),
+ SCALE_TRANSLATE(4, hasRotation = false, hasScale = true, hasTranslation = true),
+ COMPLEX(5, hasRotation = true, hasScale = true, hasTranslation = true);
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix3.kt b/math/src/main/java/com/icegps/math/geometry/Matrix3.kt
new file mode 100644
index 00000000..5c2cacca
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Matrix3.kt
@@ -0,0 +1,237 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import kotlin.math.*
+
+/**
+ * Useful for representing rotations and scales.
+ */
+data class Matrix3 private constructor(
+ internal val data: FloatArray,
+) : IsAlmostEqualsF {
+ override fun equals(other: Any?): Boolean = other is Matrix3 && this.data.contentEquals(other.data)
+ override fun hashCode(): Int = data.contentHashCode()
+
+ private constructor(
+ v00: Float, v10: Float, v20: Float,
+ v01: Float, v11: Float, v21: Float,
+ v02: Float, v12: Float, v22: Float,
+ ) : this(
+ floatArrayOf(
+ v00, v10, v20,
+ v01, v11, v21,
+ v02, v12, v22,
+ )
+ )
+
+ init {
+ check(data.size == 9)
+ }
+
+ val v00: Float get() = data[0]
+ val v10: Float get() = data[1]
+ val v20: Float get() = data[2]
+ val v01: Float get() = data[3]
+ val v11: Float get() = data[4]
+ val v21: Float get() = data[5]
+ val v02: Float get() = data[6]
+ val v12: Float get() = data[7]
+ val v22: Float get() = data[8]
+
+ val c0: Vector3F get() = Vector3F.fromArray(data, 0)
+ val c1: Vector3F get() = Vector3F.fromArray(data, 3)
+ val c2: Vector3F get() = Vector3F.fromArray(data, 6)
+ fun c(column: Int): Vector3F {
+ if (column < 0 || column >= 3) error("Invalid column $column")
+ return Vector3F.fromArray(data, column * 3)
+ }
+
+ val r0: Vector3F get() = Vector3F(v00, v01, v02)
+ val r1: Vector3F get() = Vector3F(v10, v11, v12)
+ val r2: Vector3F get() = Vector3F(v20, v21, v22)
+
+ fun v(index: Int): Float = data[index]
+
+ fun r(row: Int): Vector3F = when (row) {
+ 0 -> r0
+ 1 -> r1
+ 2 -> r2
+ else -> error("Invalid row $row")
+ }
+
+ operator fun get(row: Int, column: Int): Float {
+ if (column !in 0..2 || row !in 0..2) error("Invalid index $row,$column")
+ return data[row * 3 + column]
+ }
+
+ fun transform(v: Vector3F): Vector3F = Vector3F(r0.dot(v), r1.dot(v), r2.dot(v))
+
+ operator fun unaryMinus(): Matrix3 = Matrix3(
+ -v00, -v10, -v20,
+ -v01, -v11, -v21,
+ -v02, -v12, -v22,
+ )
+ operator fun unaryPlus(): Matrix3 = this
+
+ operator fun minus(other: Matrix3): Matrix3 = Matrix3(
+ v00 - other.v00, v10 - other.v10, v20 - other.v20,
+ v01 - other.v01, v11 - other.v11, v21 - other.v21,
+ v02 - other.v02, v12 - other.v12, v22 - other.v22,
+ )
+ operator fun plus(other: Matrix3): Matrix3 = Matrix3(
+ v00 + other.v00, v10 + other.v10, v20 + other.v20,
+ v01 + other.v01, v11 + other.v11, v21 + other.v21,
+ v02 + other.v02, v12 + other.v12, v22 + other.v22,
+ )
+
+ operator fun times(other: Matrix3): Matrix3 = Matrix3.multiply(this, other)
+ operator fun times(scale: Float): Matrix3 = Matrix3(
+ v00 * scale, v10 * scale, v20 * scale,
+ v01 * scale, v11 * scale, v21 * scale,
+ v02 * scale, v12 * scale, v22 * scale,
+ )
+ operator fun div(scale: Float): Matrix3 = this * (1f / scale)
+
+ fun inv(): Matrix3 = inverted()
+
+ val determinant: Float get() = v00 * (v11 * v22 - v21 * v12) -
+ v01 * (v10 * v22 - v12 * v20) +
+ v02 * (v10 * v21 - v11 * v20)
+
+ fun inverted(): Matrix3 {
+ val determinant = this.determinant
+
+ if (determinant == 0.0f) throw ArithmeticException("Matrix is not invertible")
+
+ val invDet = 1.0f / determinant
+
+ return fromRows(
+ (v11 * v22 - v21 * v12) * invDet,
+ (v02 * v21 - v01 * v22) * invDet,
+ (v01 * v12 - v02 * v11) * invDet,
+ (v12 * v20 - v10 * v22) * invDet,
+ (v00 * v22 - v02 * v20) * invDet,
+ (v10 * v02 - v00 * v12) * invDet,
+ (v10 * v21 - v20 * v11) * invDet,
+ (v20 * v01 - v00 * v21) * invDet,
+ (v00 * v11 - v10 * v01) * invDet,
+ )
+ }
+
+ override fun toString(): String = buildString {
+ append("Matrix3(\n")
+ for (row in 0 until 3) {
+ append(" [ ")
+ for (col in 0 until 3) {
+ if (col != 0) append(", ")
+ val v = get(row, col)
+ if (floor(v) == v) append(v.toInt()) else append(v)
+ }
+ append(" ],\n")
+ }
+ append(")")
+ }
+
+ fun transposed(): Matrix3 = Matrix3.fromColumns(r0, r1, r2)
+
+ override fun isAlmostEquals(other: Matrix3, epsilon: Float): Boolean = c0.isAlmostEquals(other.c0, epsilon)
+ && c1.isAlmostEquals(other.c1, epsilon)
+ && c2.isAlmostEquals(other.c2, epsilon)
+
+ companion object {
+ const val M00 = 0
+ const val M10 = 1
+ const val M20 = 2
+
+ const val M01 = 3
+ const val M11 = 4
+ const val M21 = 5
+
+ const val M02 = 6
+ const val M12 = 7
+ const val M22 = 8
+
+ const val M03 = 9
+ const val M13 = 10
+ const val M23 = 11
+
+ val INDICES_BY_COLUMNS = intArrayOf(
+ M00, M10, M20,
+ M01, M11, M21,
+ M02, M12, M22,
+ )
+ val INDICES_BY_ROWS = intArrayOf(
+ M00, M01, M02,
+ M10, M11, M12,
+ M20, M21, M22,
+ )
+
+ val IDENTITY = Matrix3(
+ 1f, 0f, 0f,
+ 0f, 1f, 0f,
+ 0f, 0f, 1f,
+ )
+
+ fun fromRows(
+ r0: Vector3F, r1: Vector3F, r2: Vector3F
+ ): Matrix3 = Matrix3(
+ r0.x, r1.x, r2.x,
+ r0.y, r1.y, r2.y,
+ r0.z, r1.z, r2.z,
+ )
+
+ fun fromColumns(
+ c0: Vector3F, c1: Vector3F, c2: Vector3F
+ ): Matrix3 = Matrix3(
+ c0.x, c0.y, c0.z,
+ c1.x, c1.y, c1.z,
+ c2.x, c2.y, c2.z,
+ )
+
+ fun fromColumns(
+ v00: Float, v10: Float, v20: Float,
+ v01: Float, v11: Float, v21: Float,
+ v02: Float, v12: Float, v22: Float,
+ ): Matrix3 = Matrix3(
+ v00, v10, v20,
+ v01, v11, v21,
+ v02, v12, v22,
+ )
+
+ fun fromRows(
+ v00: Float, v01: Float, v02: Float,
+ v10: Float, v11: Float, v12: Float,
+ v20: Float, v21: Float, v22: Float,
+ ): Matrix3 = Matrix3(
+ v00, v10, v20,
+ v01, v11, v21,
+ v02, v12, v22,
+ )
+
+ fun multiply(l: Matrix3, r: Matrix3): Matrix3 = Matrix3.fromRows(
+ (l.v00 * r.v00) + (l.v01 * r.v10) + (l.v02 * r.v20),
+ (l.v00 * r.v01) + (l.v01 * r.v11) + (l.v02 * r.v21),
+ (l.v00 * r.v02) + (l.v01 * r.v12) + (l.v02 * r.v22),
+
+ (l.v10 * r.v00) + (l.v11 * r.v10) + (l.v12 * r.v20),
+ (l.v10 * r.v01) + (l.v11 * r.v11) + (l.v12 * r.v21),
+ (l.v10 * r.v02) + (l.v11 * r.v12) + (l.v12 * r.v22),
+
+ (l.v20 * r.v00) + (l.v21 * r.v10) + (l.v22 * r.v20),
+ (l.v20 * r.v01) + (l.v21 * r.v11) + (l.v22 * r.v21),
+ (l.v20 * r.v02) + (l.v21 * r.v12) + (l.v22 * r.v22),
+ )
+ }
+}
+
+fun Matrix3.toMatrix4(): Matrix4 = Matrix4.fromRows(
+ v00, v01, v02, 0f,
+ v10, v11, v12, 0f,
+ v20, v21, v22, 0f,
+ 0f, 0f, 0f, 1f,
+)
+
+fun Matrix3.toQuaternion(): Quaternion = Quaternion.fromRotationMatrix(this)
+
diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix4.kt b/math/src/main/java/com/icegps/math/geometry/Matrix4.kt
new file mode 100644
index 00000000..5aa96bea
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Matrix4.kt
@@ -0,0 +1,684 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.geometry.Matrix4.*
+import kotlin.math.*
+
+
+// @TODO: WIP
+// @TODO: value class
+// Stored as four consecutive column vectors (effectively stored in column-major order) see https://en.wikipedia.org/wiki/Row-_and_column-major_order
+// v[Row][Column]
+//@KormaExperimental
+//@KormaValueApi
+//inline class Matrix4 private constructor(
+/**
+ * Useful for representing complete transforms: rotations, scales, translations, projections, etc.
+ */
+data class Matrix4 private constructor(
+ private val data: FloatArray,
+ //val c0: Vector4, val c1: Vector4, val c2: Vector4, val c3: Vector4,
+
+ //val v00: Float, val v10: Float, val v20: Float, val v30: Float,
+ //val v01: Float, val v11: Float, val v21: Float, val v31: Float,
+ //val v02: Float, val v12: Float, val v22: Float, val v32: Float,
+ //val v03: Float, val v13: Float, val v23: Float, val v33: Float,
+) : IsAlmostEqualsF {
+ init {
+ check(data.size == 16)
+ }
+ val v00: Float get() = data[0]; val v10: Float get() = data[1]; val v20: Float get() = data[2]; val v30: Float get() = data[3]
+ val v01: Float get() = data[4]; val v11: Float get() = data[5]; val v21: Float get() = data[6]; val v31: Float get() = data[7]
+ val v02: Float get() = data[8]; val v12: Float get() = data[9]; val v22: Float get() = data[10]; val v32: Float get() = data[11]
+ val v03: Float get() = data[12]; val v13: Float get() = data[13]; val v23: Float get() = data[14]; val v33: Float get() = data[15]
+
+ override fun equals(other: Any?): Boolean = other is Matrix4 && this.data.contentEquals(other.data)
+ override fun hashCode(): Int = data.contentHashCode()
+
+ operator fun times(scale: Float): Matrix4 = Matrix4.fromColumns(c0 * scale, c1 * scale, c2 * scale, c3 * scale)
+ operator fun times(that: Matrix4): Matrix4 = Matrix4.multiply(this, that)
+
+ fun transformTransposed(v: Vector4F): Vector4F = Vector4F(c0.dot(v), c1.dot(v), c2.dot(v), c3.dot(v))
+ fun transform(v: Vector4F): Vector4F = Vector4F(r0.dot(v), r1.dot(v), r2.dot(v), r3.dot(v))
+ fun transform(v: Vector3F): Vector3F = transform(v.toVector4()).toVector3()
+
+ fun transposed(): Matrix4 = Matrix4.fromColumns(r0, r1, r2, r3)
+
+ val determinant: Float get() = 0f +
+ (v30 * v21 * v12 * v03) -
+ (v20 * v31 * v12 * v03) -
+ (v30 * v11 * v22 * v03) +
+ (v10 * v31 * v22 * v03) +
+ (v20 * v11 * v32 * v03) -
+ (v10 * v21 * v32 * v03) -
+ (v30 * v21 * v02 * v13) +
+ (v20 * v31 * v02 * v13) +
+ (v30 * v01 * v22 * v13) -
+ (v00 * v31 * v22 * v13) -
+ (v20 * v01 * v32 * v13) +
+ (v00 * v21 * v32 * v13) +
+ (v30 * v11 * v02 * v23) -
+ (v10 * v31 * v02 * v23) -
+ (v30 * v01 * v12 * v23) +
+ (v00 * v31 * v12 * v23) +
+ (v10 * v01 * v32 * v23) -
+ (v00 * v11 * v32 * v23) -
+ (v20 * v11 * v02 * v33) +
+ (v10 * v21 * v02 * v33) +
+ (v20 * v01 * v12 * v33) -
+ (v00 * v21 * v12 * v33) -
+ (v10 * v01 * v22 * v33) +
+ (v00 * v11 * v22 * v33)
+
+ // Use toTRS/decompose
+ //fun decomposeProjection(): Vector4 = c3
+ //fun decomposeTranslation(): Vector4 = r3.copy(w = 1f)
+ //fun decomposeScale(): Vector4 {
+ // val x = r0.length3
+ // val y = r1.length3
+ // val z = r2.length3
+ // return Vector4(x, y, z, 1f)
+ //}
+ fun decomposeRotation(rowNormalise: Boolean = true): Quaternion {
+ var v1 = this.r0
+ var v2 = this.r1
+ var v3 = this.r2
+ if (rowNormalise) {
+ v1 = v1.normalized()
+ v2 = v2.normalized()
+ v3 = v3.normalized()
+ }
+ val d: Float = 0.25f * (v1[0] + v2[1] + v3[2] + 1f)
+ val out: Vector4F
+ when {
+ d > 0f -> {
+ val num1: Float = sqrt(d)
+ val num2: Float = 1f / (4f * num1)
+ out = Vector4F(
+ ((v2[2] - v3[1]) * num2),
+ ((v3[0] - v1[2]) * num2),
+ ((v1[1] - v2[0]) * num2),
+ num1,
+ )
+ }
+ v1[0] > v2[1] && v1[0] > v3[2] -> {
+ val num1: Float = 2f * sqrt(1f + v1[0] - v2[1] - v3[2])
+ val num2: Float = 1f / num1
+ out = Vector4F(
+ (0.25f * num1),
+ ((v2[0] + v1[1]) * num2),
+ ((v3[0] + v1[2]) * num2),
+ ((v3[1] - v2[2]) * num2),
+ )
+ }
+ v2[1] > v3[2] -> {
+ val num5: Float = 2f * sqrt(1f + v2[1] - v1[0] - v3[2])
+ val num6: Float = 1f / num5
+ out = Vector4F(
+ ((v2[0] + v1[1]) * num6),
+ (0.25f * num5),
+ ((v3[1] + v2[2]) * num6),
+ ((v3[0] - v1[2]) * num6),
+ )
+ }
+ else -> {
+ val num7: Float = 2f * sqrt(1f + v3[2] - v1[0] - v2[1])
+ val num8: Float = 1f / num7
+ out = Vector4F(
+ ((v3[0] + v1[2]) * num8),
+ ((v3[1] + v2[2]) * num8),
+ (0.25f * num7),
+ ((v2[0] - v1[1]) * num8),
+ )
+ }
+ }
+ return Quaternion(out.normalized())
+ }
+
+ fun copyToColumns(out: FloatArray = FloatArray(16), offset: Int = 0): FloatArray {
+ this.data.copyInto(out, offset, 0, 16)
+ return out
+ }
+ fun copyToRows(out: FloatArray = FloatArray(16), offset: Int = 0): FloatArray {
+ this.r0.copyTo(out, offset + 0)
+ this.r1.copyTo(out, offset + 4)
+ this.r2.copyTo(out, offset + 8)
+ this.r3.copyTo(out, offset + 12)
+ return out
+ }
+
+ private constructor(
+ v00: Float, v10: Float, v20: Float, v30: Float,
+ v01: Float, v11: Float, v21: Float, v31: Float,
+ v02: Float, v12: Float, v22: Float, v32: Float,
+ v03: Float, v13: Float, v23: Float, v33: Float,
+ ) : this(floatArrayOf(
+ v00, v10, v20, v30,
+ v01, v11, v21, v31,
+ v02, v12, v22, v32,
+ v03, v13, v23, v33,
+ ))
+
+ constructor() : this(
+ 1f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f,
+ 0f, 0f, 1f, 0f,
+ 0f, 0f, 0f, 1f,
+ )
+
+ val c0: Vector4F get() = Vector4F.fromArray(data, 0)
+ val c1: Vector4F get() = Vector4F.fromArray(data, 4)
+ val c2: Vector4F get() = Vector4F.fromArray(data, 8)
+ val c3: Vector4F get() = Vector4F.fromArray(data, 12)
+ fun c(column: Int): Vector4F {
+ if (column < 0 || column >= 4) error("Invalid column $column")
+ return Vector4F.fromArray(data, column * 4)
+ }
+
+ val r0: Vector4F get() = Vector4F(v00, v01, v02, v03)
+ val r1: Vector4F get() = Vector4F(v10, v11, v12, v13)
+ val r2: Vector4F get() = Vector4F(v20, v21, v22, v23)
+ val r3: Vector4F get() = Vector4F(v30, v31, v32, v33)
+
+ fun r(row: Int): Vector4F = when (row) {
+ 0 -> r0
+ 1 -> r1
+ 2 -> r2
+ 3 -> r3
+ else -> error("Invalid row $row")
+ }
+
+ operator fun get(row: Int, column: Int): Float {
+ if (column !in 0..3 || row !in 0..3) error("Invalid index $row,$column")
+ return data[row * 4 + column]
+ }
+
+ fun getAtIndex(index: Int): Float {
+ if (index !in data.indices) error("Invalid index $index")
+ return data[index]
+ }
+
+ override fun toString(): String = buildString {
+ append("Matrix4(\n")
+ for (row in 0 until 4) {
+ append(" [ ")
+ for (col in 0 until 4) {
+ if (col != 0) append(", ")
+ val v = get(row, col)
+ if (floor(v) == v) append(v.toInt()) else append(v)
+ }
+ append(" ],\n")
+ }
+ append(")")
+ }
+
+
+
+ fun translated(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = this * Matrix4.translation(x, y, z, w)
+ fun translated(x: Double, y: Double, z: Double, w: Double = 1.0) = this.translated(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+ fun translated(x: Int, y: Int, z: Int, w: Int = 1) = this.translated(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ fun rotated(angle: Angle, x: Float, y: Float, z: Float): Matrix4 = this * Matrix4.rotation(angle, x, y, z)
+ fun rotated(angle: Angle, x: Double, y: Double, z: Double): Matrix4 = this.rotated(angle, x.toFloat(), y.toFloat(), z.toFloat())
+ fun rotated(angle: Angle, x: Int, y: Int, z: Int): Matrix4 = this.rotated(angle, x.toFloat(), y.toFloat(), z.toFloat())
+
+ fun scaled(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = this * Matrix4.scale(x, y, z, w)
+ fun scaled(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = this.scaled(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+ fun scaled(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = this.scaled(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ fun rotated(quat: Quaternion): Matrix4 = this * quat.toMatrix()
+ fun rotated(euler: EulerRotation): Matrix4 = this * euler.toMatrix()
+ fun rotated(x: Angle, y: Angle, z: Angle): Matrix4 = rotated(x, 1f, 0f, 0f).rotated(y, 0f, 1f, 0f).rotated(z, 0f, 0f, 1f)
+
+ fun decompose(): TRS4 = toTRS()
+ fun toTRS(): TRS4 {
+ val det = determinant
+ val translation = Vector4F(v03, v13, v23, 1f)
+ val scale = Vector4F(Vector3F.length(v00, v10, v20) * det.sign, Vector3F.length(v01, v11, v21), Vector3F.length(v02, v12, v22), 1f)
+ val invSX = 1f / scale.x
+ val invSY = 1f / scale.y
+ val invSZ = 1f / scale.z
+ val rotation = Quaternion.fromRotationMatrix(Matrix4.fromRows(
+ v00 * invSX, v01 * invSY, v02 * invSZ, v03,
+ v10 * invSX, v11 * invSY, v12 * invSZ, v13,
+ v20 * invSX, v21 * invSY, v22 * invSZ, v23,
+ v30, v31, v32, v33
+ ))
+ return TRS4(translation, rotation, scale)
+ }
+
+ fun inverted(): Matrix4 {
+ val t11 = v12 * v23 * v31 - v13 * v22 * v31 + v13 * v21 * v32 - v11 * v23 * v32 - v12 * v21 * v33 + v11 * v22 * v33
+ val t12 = v03 * v22 * v31 - v02 * v23 * v31 - v03 * v21 * v32 + v01 * v23 * v32 + v02 * v21 * v33 - v01 * v22 * v33
+ val t13 = v02 * v13 * v31 - v03 * v12 * v31 + v03 * v11 * v32 - v01 * v13 * v32 - v02 * v11 * v33 + v01 * v12 * v33
+ val t14 = v03 * v12 * v21 - v02 * v13 * v21 - v03 * v11 * v22 + v01 * v13 * v22 + v02 * v11 * v23 - v01 * v12 * v23
+
+ val det = v00 * t11 + v10 * t12 + v20 * t13 + v30 * t14
+
+ if (det == 0f) {
+ println("Matrix doesn't have inverse")
+ return Matrix4.IDENTITY
+ }
+
+ val detInv = 1 / det
+
+ return Matrix4.fromRows(
+ t11 * detInv,
+ t12 * detInv,
+ t13 * detInv,
+ t14 * detInv,
+
+ (v13 * v22 * v30 - v12 * v23 * v30 - v13 * v20 * v32 + v10 * v23 * v32 + v12 * v20 * v33 - v10 * v22 * v33) * detInv,
+ (v02 * v23 * v30 - v03 * v22 * v30 + v03 * v20 * v32 - v00 * v23 * v32 - v02 * v20 * v33 + v00 * v22 * v33) * detInv,
+ (v03 * v12 * v30 - v02 * v13 * v30 - v03 * v10 * v32 + v00 * v13 * v32 + v02 * v10 * v33 - v00 * v12 * v33) * detInv,
+ (v02 * v13 * v20 - v03 * v12 * v20 + v03 * v10 * v22 - v00 * v13 * v22 - v02 * v10 * v23 + v00 * v12 * v23) * detInv,
+
+ (v11 * v23 * v30 - v13 * v21 * v30 + v13 * v20 * v31 - v10 * v23 * v31 - v11 * v20 * v33 + v10 * v21 * v33) * detInv,
+ (v03 * v21 * v30 - v01 * v23 * v30 - v03 * v20 * v31 + v00 * v23 * v31 + v01 * v20 * v33 - v00 * v21 * v33) * detInv,
+ (v01 * v13 * v30 - v03 * v11 * v30 + v03 * v10 * v31 - v00 * v13 * v31 - v01 * v10 * v33 + v00 * v11 * v33) * detInv,
+ (v03 * v11 * v20 - v01 * v13 * v20 - v03 * v10 * v21 + v00 * v13 * v21 + v01 * v10 * v23 - v00 * v11 * v23) * detInv,
+
+ (v12 * v21 * v30 - v11 * v22 * v30 - v12 * v20 * v31 + v10 * v22 * v31 + v11 * v20 * v32 - v10 * v21 * v32) * detInv,
+ (v01 * v22 * v30 - v02 * v21 * v30 + v02 * v20 * v31 - v00 * v22 * v31 - v01 * v20 * v32 + v00 * v21 * v32) * detInv,
+ (v02 * v11 * v30 - v01 * v12 * v30 - v02 * v10 * v31 + v00 * v12 * v31 + v01 * v10 * v32 - v00 * v11 * v32) * detInv,
+ (v01 * v12 * v20 - v02 * v11 * v20 + v02 * v10 * v21 - v00 * v12 * v21 - v01 * v10 * v22 + v00 * v11 * v22) * detInv
+ )
+ }
+
+ override fun isAlmostEquals(other: Matrix4, epsilon: Float): Boolean =
+ c0.isAlmostEquals(other.c0, epsilon) &&
+ c1.isAlmostEquals(other.c1, epsilon) &&
+ c2.isAlmostEquals(other.c2, epsilon) &&
+ c3.isAlmostEquals(other.c3, epsilon)
+
+ companion object {
+ const val M00 = 0
+ const val M10 = 1
+ const val M20 = 2
+ const val M30 = 3
+
+ const val M01 = 4
+ const val M11 = 5
+ const val M21 = 6
+ const val M31 = 7
+
+ const val M02 = 8
+ const val M12 = 9
+ const val M22 = 10
+ const val M32 = 11
+
+ const val M03 = 12
+ const val M13 = 13
+ const val M23 = 14
+ const val M33 = 15
+
+ val INDICES_BY_COLUMNS_4x4 = intArrayOf(
+ M00, M10, M20, M30,
+ M01, M11, M21, M31,
+ M02, M12, M22, M32,
+ M03, M13, M23, M33,
+ )
+ val INDICES_BY_ROWS_4x4 = intArrayOf(
+ M00, M01, M02, M03,
+ M10, M11, M12, M13,
+ M20, M21, M22, M23,
+ M30, M31, M32, M33,
+ )
+ val INDICES_BY_COLUMNS_3x3 = intArrayOf(
+ M00, M10, M20,
+ M01, M11, M21,
+ M02, M12, M22,
+ )
+ val INDICES_BY_ROWS_3x3 = intArrayOf(
+ M00, M01, M02,
+ M10, M11, M12,
+ M20, M21, M22,
+ )
+
+ val IDENTITY = Matrix4()
+
+ fun fromColumns(
+ c0: Vector4F, c1: Vector4F, c2: Vector4F, c3: Vector4F
+ ): Matrix4 = Matrix4(
+ c0.x, c0.y, c0.z, c0.w,
+ c1.x, c1.y, c1.z, c1.w,
+ c2.x, c2.y, c2.z, c2.w,
+ c3.x, c3.y, c3.z, c3.w,
+ )
+
+ fun fromColumns(v: FloatArray, offset: Int = 0): Matrix4 = Matrix4.fromColumns(
+ v[offset + 0], v[offset + 1], v[offset + 2], v[offset + 3],
+ v[offset + 4], v[offset + 5], v[offset + 6], v[offset + 7],
+ v[offset + 8], v[offset + 9], v[offset + 10], v[offset + 11],
+ v[offset + 12], v[offset + 13], v[offset + 14], v[offset + 15],
+ )
+
+ fun fromRows(v: FloatArray, offset: Int = 0): Matrix4 = Matrix4.fromRows(
+ v[offset + 0], v[offset + 1], v[offset + 2], v[offset + 3],
+ v[offset + 4], v[offset + 5], v[offset + 6], v[offset + 7],
+ v[offset + 8], v[offset + 9], v[offset + 10], v[offset + 11],
+ v[offset + 12], v[offset + 13], v[offset + 14], v[offset + 15],
+ )
+
+ fun fromRows(
+ r0: Vector4F, r1: Vector4F, r2: Vector4F, r3: Vector4F
+ ): Matrix4 = Matrix4(
+ r0.x, r1.x, r2.x, r3.x,
+ r0.y, r1.y, r2.y, r3.y,
+ r0.z, r1.z, r2.z, r3.z,
+ r0.w, r1.w, r2.w, r3.w,
+ )
+
+ fun fromColumns(
+ v00: Float, v10: Float, v20: Float, v30: Float,
+ v01: Float, v11: Float, v21: Float, v31: Float,
+ v02: Float, v12: Float, v22: Float, v32: Float,
+ v03: Float, v13: Float, v23: Float, v33: Float,
+ ): Matrix4 = Matrix4(
+ v00, v10, v20, v30,
+ v01, v11, v21, v31,
+ v02, v12, v22, v32,
+ v03, v13, v23, v33,
+ )
+
+ fun fromRows(
+ v00: Float, v01: Float, v02: Float, v03: Float,
+ v10: Float, v11: Float, v12: Float, v13: Float,
+ v20: Float, v21: Float, v22: Float, v23: Float,
+ v30: Float, v31: Float, v32: Float, v33: Float,
+ ): Matrix4 = Matrix4(
+ v00, v10, v20, v30,
+ v01, v11, v21, v31,
+ v02, v12, v22, v32,
+ v03, v13, v23, v33,
+ )
+
+ fun fromRows3x3(
+ a00: Float, a01: Float, a02: Float,
+ a10: Float, a11: Float, a12: Float,
+ a20: Float, a21: Float, a22: Float
+ ): Matrix4 = Matrix4.fromRows(
+ a00, a01, a02, 0f,
+ a10, a11, a12, 0f,
+ a20, a21, a22, 0f,
+ 0f, 0f, 0f, 1f,
+ )
+
+ fun fromColumns3x3(
+ a00: Float, a10: Float, a20: Float,
+ a01: Float, a11: Float, a21: Float,
+ a02: Float, a12: Float, a22: Float
+ ): Matrix4 = Matrix4.fromColumns(
+ a00, a10, a20, 0f,
+ a01, a11, a21, 0f,
+ a02, a12, a22, 0f,
+ 0f, 0f, 0f, 1f,
+ )
+
+ fun fromTRS(trs: TRS4): Matrix4 = fromTRS(trs.translation, trs.rotation, trs.scale)
+ fun fromTRS(translation: Vector4F, rotation: Quaternion, scale: Vector4F): Matrix4 {
+ val rx = rotation.x
+ val ry = rotation.y
+ val rz = rotation.z
+ val rw = rotation.w
+
+ val xt = rx + rx
+ val yt = ry + ry
+ val zt = rz + rz
+
+ val xx = rx * xt
+ val xy = rx * yt
+ val xz = rx * zt
+
+ val yy = ry * yt
+ val yz = ry * zt
+ val zz = rz * zt
+
+ val wx = rw * xt
+ val wy = rw * yt
+ val wz = rw * zt
+
+ return Matrix4.fromRows(
+ ((1 - (yy + zz)) * scale.x), ((xy - wz) * scale.y), ((xz + wy) * scale.z), translation.x,
+ ((xy + wz) * scale.x), ((1 - (xx + zz)) * scale.y), ((yz - wx) * scale.z), translation.y,
+ ((xz - wy) * scale.x), ((yz + wx) * scale.y), ((1 - (xx + yy)) * scale.z), translation.z,
+ 0f, 0f, 0f, 1f
+ )
+ }
+
+ fun translation(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = Matrix4.fromRows(
+ 1f, 0f, 0f, x,
+ 0f, 1f, 0f, y,
+ 0f, 0f, 1f, z,
+ 0f, 0f, 0f, w
+ )
+ fun translation(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = translation(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+ fun translation(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = translation(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ fun scale(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = Matrix4.fromRows(
+ x, 0f, 0f, 0f,
+ 0f, y, 0f, 0f,
+ 0f, 0f, z, 0f,
+ 0f, 0f, 0f, w
+ )
+ fun scale(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = scale(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+ fun scale(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = scale(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ fun shear(x: Float, y: Float, z: Float): Matrix4 = fromRows(
+ 1f, y, z, 0f,
+ x, 1f, z, 0f,
+ x, y, 1f, 0f,
+ 0f, 0f, 0f, 1f
+ )
+ fun shear(x: Double, y: Double, z: Double): Matrix4 = shear(x.toFloat(), y.toFloat(), z.toFloat())
+ fun shear(x: Int, y: Int, z: Int): Matrix4 = shear(x.toFloat(), y.toFloat(), z.toFloat())
+
+ fun rotationX(angle: Angle): Matrix4 {
+ val c = angle.cosine.toFloat()
+ val s = angle.sine.toFloat()
+ return Matrix4.fromRows(
+ 1f, 0f, 0f, 0f,
+ 0f, c, -s, 0f,
+ 0f, s, c, 0f,
+ 0f, 0f, 0f, 1f
+ )
+ }
+
+ fun rotationY(angle: Angle): Matrix4 {
+ val c = angle.cosine.toFloat()
+ val s = angle.sine.toFloat()
+ return Matrix4.fromRows(
+ c, 0f, s, 0f,
+ 0f, 1f, 0f, 0f,
+ -s, 0f, c, 0f,
+ 0f, 0f, 0f, 1f
+ )
+ }
+
+ fun rotationZ(angle: Angle): Matrix4 {
+ val c = angle.cosine.toFloat()
+ val s = angle.sine.toFloat()
+ return Matrix4.fromRows(
+ c, -s, 0f, 0f,
+ s, c, 0f, 0f,
+ 0f, 0f, 1f, 0f,
+ 0f, 0f, 0f, 1f
+ )
+ }
+
+ fun rotation(angle: Angle, x: Float, y: Float, z: Float): Matrix4 {
+ val mag = sqrt(x * x + y * y + z * z)
+ val norm = 1f / mag
+
+ val nx = x * norm
+ val ny = y * norm
+ val nz = z * norm
+ val c = angle.cosine.toFloat()
+ val s = angle.sine.toFloat()
+ val t = 1 - c
+ val tx = t * nx
+ val ty = t * ny
+
+ return Matrix4.fromRows(
+ tx * nx + c, tx * ny - s * nz, tx * nz + s * ny, 0f,
+ tx * ny + s * nz, ty * ny + c, ty * nz - s * nx, 0f,
+ tx * nz - s * ny, ty * nz + s * nx, t * nz * nz + c, 0f,
+ 0f, 0f, 0f, 1f
+ )
+ }
+ fun rotation(angle: Angle, direction: Vector3F): Matrix4 = rotation(angle, direction.x, direction.y, direction.z)
+ fun rotation(angle: Angle, x: Double, y: Double, z: Double): Matrix4 = rotation(angle, x.toFloat(), y.toFloat(), z.toFloat())
+ fun rotation(angle: Angle, x: Int, y: Int, z: Int): Matrix4 = rotation(angle, x.toFloat(), y.toFloat(), z.toFloat())
+
+ // @TODO: Use Vector4 operations, and use columns instead of rows for faster set
+ fun multiply(l: Matrix4, r: Matrix4): Matrix4 = Matrix4.fromRows(
+ (l.v00 * r.v00) + (l.v01 * r.v10) + (l.v02 * r.v20) + (l.v03 * r.v30),
+ (l.v00 * r.v01) + (l.v01 * r.v11) + (l.v02 * r.v21) + (l.v03 * r.v31),
+ (l.v00 * r.v02) + (l.v01 * r.v12) + (l.v02 * r.v22) + (l.v03 * r.v32),
+ (l.v00 * r.v03) + (l.v01 * r.v13) + (l.v02 * r.v23) + (l.v03 * r.v33),
+
+ (l.v10 * r.v00) + (l.v11 * r.v10) + (l.v12 * r.v20) + (l.v13 * r.v30),
+ (l.v10 * r.v01) + (l.v11 * r.v11) + (l.v12 * r.v21) + (l.v13 * r.v31),
+ (l.v10 * r.v02) + (l.v11 * r.v12) + (l.v12 * r.v22) + (l.v13 * r.v32),
+ (l.v10 * r.v03) + (l.v11 * r.v13) + (l.v12 * r.v23) + (l.v13 * r.v33),
+
+ (l.v20 * r.v00) + (l.v21 * r.v10) + (l.v22 * r.v20) + (l.v23 * r.v30),
+ (l.v20 * r.v01) + (l.v21 * r.v11) + (l.v22 * r.v21) + (l.v23 * r.v31),
+ (l.v20 * r.v02) + (l.v21 * r.v12) + (l.v22 * r.v22) + (l.v23 * r.v32),
+ (l.v20 * r.v03) + (l.v21 * r.v13) + (l.v22 * r.v23) + (l.v23 * r.v33),
+
+ (l.v30 * r.v00) + (l.v31 * r.v10) + (l.v32 * r.v20) + (l.v33 * r.v30),
+ (l.v30 * r.v01) + (l.v31 * r.v11) + (l.v32 * r.v21) + (l.v33 * r.v31),
+ (l.v30 * r.v02) + (l.v31 * r.v12) + (l.v32 * r.v22) + (l.v33 * r.v32),
+ (l.v30 * r.v03) + (l.v31 * r.v13) + (l.v32 * r.v23) + (l.v33 * r.v33)
+ )
+
+ fun multiply(
+ lv00: Float, lv01: Float, lv02: Float, lv03: Float,
+ lv10: Float, lv11: Float, lv12: Float, lv13: Float,
+ lv20: Float, lv21: Float, lv22: Float, lv23: Float,
+ lv30: Float, lv31: Float, lv32: Float, lv33: Float,
+
+ rv00: Float, rv01: Float, rv02: Float, rv03: Float,
+ rv10: Float, rv11: Float, rv12: Float, rv13: Float,
+ rv20: Float, rv21: Float, rv22: Float, rv23: Float,
+ rv30: Float, rv31: Float, rv32: Float, rv33: Float,
+ ): Matrix4 = Matrix4.fromRows(
+ (lv00 * rv00) + (lv01 * rv10) + (lv02 * rv20) + (lv03 * rv30),
+ (lv00 * rv01) + (lv01 * rv11) + (lv02 * rv21) + (lv03 * rv31),
+ (lv00 * rv02) + (lv01 * rv12) + (lv02 * rv22) + (lv03 * rv32),
+ (lv00 * rv03) + (lv01 * rv13) + (lv02 * rv23) + (lv03 * rv33),
+
+ (lv10 * rv00) + (lv11 * rv10) + (lv12 * rv20) + (lv13 * rv30),
+ (lv10 * rv01) + (lv11 * rv11) + (lv12 * rv21) + (lv13 * rv31),
+ (lv10 * rv02) + (lv11 * rv12) + (lv12 * rv22) + (lv13 * rv32),
+ (lv10 * rv03) + (lv11 * rv13) + (lv12 * rv23) + (lv13 * rv33),
+
+ (lv20 * rv00) + (lv21 * rv10) + (lv22 * rv20) + (lv23 * rv30),
+ (lv20 * rv01) + (lv21 * rv11) + (lv22 * rv21) + (lv23 * rv31),
+ (lv20 * rv02) + (lv21 * rv12) + (lv22 * rv22) + (lv23 * rv32),
+ (lv20 * rv03) + (lv21 * rv13) + (lv22 * rv23) + (lv23 * rv33),
+
+ (lv30 * rv00) + (lv31 * rv10) + (lv32 * rv20) + (lv33 * rv30),
+ (lv30 * rv01) + (lv31 * rv11) + (lv32 * rv21) + (lv33 * rv31),
+ (lv30 * rv02) + (lv31 * rv12) + (lv32 * rv22) + (lv33 * rv32),
+ (lv30 * rv03) + (lv31 * rv13) + (lv32 * rv23) + (lv33 * rv33)
+ )
+
+ fun ortho(left: Float, right: Float, bottom: Float, top: Float, near: Float = 0f, far: Float = 1f): Matrix4 {
+ val sx = 2f / (right - left)
+ val sy = 2f / (top - bottom)
+ val sz = -2f / (far - near)
+
+ val tx = -(right + left) / (right - left)
+ val ty = -(top + bottom) / (top - bottom)
+ val tz = -(far + near) / (far - near)
+
+ return Matrix4.fromRows(
+ sx, 0f, 0f, tx,
+ 0f, sy, 0f, ty,
+ 0f, 0f, sz, tz,
+ 0f, 0f, 0f, 1f
+ )
+ }
+ fun ortho(left: Double, right: Double, bottom: Double, top: Double, near: Double, far: Double): Matrix4 =
+ ortho(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), near.toFloat(), far.toFloat())
+ fun ortho(left: Int, right: Int, bottom: Int, top: Int, near: Int, far: Int): Matrix4 =
+ ortho(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), near.toFloat(), far.toFloat())
+
+ fun frustum(left: Float, right: Float, bottom: Float, top: Float, zNear: Float = 0f, zFar: Float = 1f): Matrix4 {
+ if (zNear <= 0.0f || zFar <= zNear) {
+ throw Exception("Error: Required zNear > 0 and zFar > zNear, but zNear $zNear, zFar $zFar")
+ }
+ if (left == right || top == bottom) {
+ throw Exception("Error: top,bottom and left,right must not be equal")
+ }
+
+ val zNear2 = 2.0f * zNear
+ val dx = right - left
+ val dy = top - bottom
+ val dz = zFar - zNear
+ val A = (right + left) / dx
+ val B = (top + bottom) / dy
+ val C = -1.0f * (zFar + zNear) / dz
+ val D = -2.0f * (zFar * zNear) / dz
+
+ return Matrix4.fromRows(
+ zNear2 / dx, 0f, A, 0f,
+ 0f, zNear2 / dy, B, 0f,
+ 0f, 0f, C, D,
+ 0f, 0f, -1f, 0f
+ )
+ }
+ fun frustum(left: Double, right: Double, bottom: Double, top: Double, zNear: Double = 0.0, zFar: Double = 1.0): Matrix4
+ = frustum(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), zNear.toFloat(), zFar.toFloat())
+ fun frustum(left: Int, right: Int, bottom: Int, top: Int, zNear: Int = 0, zFar: Int = 1): Matrix4
+ = frustum(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), zNear.toFloat(), zFar.toFloat())
+
+ fun perspective(fovy: Angle, aspect: Float, zNear: Float, zFar: Float): Matrix4 {
+ val top = tan(fovy.radians.toFloat() / 2f) * zNear
+ val bottom = -1.0f * top
+ val left = aspect * bottom
+ val right = aspect * top
+ return frustum(left, right, bottom, top, zNear, zFar)
+ }
+ fun perspective(fovy: Angle, aspect: Double, zNear: Double, zFar: Double): Matrix4
+ = perspective(fovy, aspect.toFloat(), zNear.toFloat(), zFar.toFloat())
+
+ fun lookAt(
+ eye: Vector3F,
+ target: Vector3F,
+ up: Vector3F
+ ): Matrix4 {
+ var z = eye - target
+ if (z.lengthSquared == 0f) z = z.copy(z = 1f)
+ z = z.normalized()
+ var x = Vector3F.cross(up, z)
+ if (x.lengthSquared == 0f) {
+ z = when {
+ abs(up.z) == 1f -> z.copy(x = z.x + 0.0001f)
+ else -> z.copy(z = z.z + 0.0001f)
+ }
+ z = z.normalized()
+ x = Vector3F.cross(up, z)
+ }
+ x = x.normalized()
+ val y = Vector3F.cross(z, x)
+ return Matrix4.fromRows(
+ x.x, y.x, z.x, 0f,
+ x.y, y.y, z.y, 0f,
+ x.z, y.z, z.z, 0f,
+ //-x.dot(eye), -y.dot(eye), -z.dot(eye), 1f // @TODO: Check why is this making other tests to fail
+ 0f, 0f, 0f, 1f
+ )
+ }
+ }
+}
+
+data class TRS4(val translation: Vector4F, val rotation: Quaternion, val scale: Vector4F)
+
+fun Matrix4.toMatrix3(): Matrix3 = Matrix3.fromRows(
+ v00, v01, v02,
+ v10, v11, v12,
+ v20, v21, v22
+)
diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt b/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt
new file mode 100644
index 00000000..cefd3a29
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt
@@ -0,0 +1,9 @@
+package com.icegps.math.geometry
+
+fun Matrix4.Companion.ortho(rect: Rectangle, near: Float = 0f, far: Float = 1f): Matrix4 = Matrix4.ortho(rect.left, rect.right, rect.bottom, rect.top, near.toDouble(), far.toDouble())
+fun Matrix4.Companion.ortho(rect: Rectangle, near: Double = 0.0, far: Double = 1.0): Matrix4 = ortho(rect, near.toFloat(), far.toFloat())
+fun Matrix4.Companion.ortho(rect: Rectangle, near: Int = 0, far: Int = 1): Matrix4 = ortho(rect, near.toFloat(), far.toFloat())
+
+fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Float = 0f, zFar: Float = 1f): Matrix4 = Matrix4.frustum(rect.left, rect.right, rect.bottom, rect.top, zNear.toDouble(), zFar.toDouble())
+fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Double = 0.0, zFar: Double = 1.0): Matrix4 = frustum(rect, zNear.toFloat(), zFar.toFloat())
+fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Int = 0, zFar: Int = 1): Matrix4 = frustum(rect, zNear.toFloat(), zFar.toFloat())
diff --git a/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt b/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt
new file mode 100644
index 00000000..405ae8a4
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt
@@ -0,0 +1,64 @@
+package com.icegps.math.geometry
+
+import kotlin.math.*
+
+fun Matrix.scaled(scale: Scale): Matrix = scaled(scale.scaleX, scale.scaleY)
+fun Matrix.prescaled(scale: Scale): Matrix = prescaled(scale.scaleX, scale.scaleY)
+
+val MatrixTransform.scale: Scale get() = Scale(scaleX, scaleY)
+
+@Suppress("DuplicatedCode")
+fun Matrix.transformRectangle(rectangle: Rectangle, delta: Boolean = false): Rectangle {
+ val a = this.a
+ val b = this.b
+ val c = this.c
+ val d = this.d
+ val tx = if (delta) 0.0 else this.tx
+ val ty = if (delta) 0.0 else this.ty
+
+ val x = rectangle.x
+ val y = rectangle.y
+ val xMax = x + rectangle.width
+ val yMax = y + rectangle.height
+
+ var x0 = a * x + c * y + tx
+ var y0 = b * x + d * y + ty
+ var x1 = a * xMax + c * y + tx
+ var y1 = b * xMax + d * y + ty
+ var x2 = a * xMax + c * yMax + tx
+ var y2 = b * xMax + d * yMax + ty
+ var x3 = a * x + c * yMax + tx
+ var y3 = b * x + d * yMax + ty
+
+ var tmp = 0.0
+
+ if (x0 > x1) {
+ tmp = x0
+ x0 = x1
+ x1 = tmp
+ }
+ if (x2 > x3) {
+ tmp = x2
+ x2 = x3
+ x3 = tmp
+ }
+
+ val rx = floor(if (x0 < x2) x0 else x2)
+ val rw = ceil((if (x1 > x3) x1 else x3) - rectangle.x)
+
+ if (y0 > y1) {
+ tmp = y0
+ y0 = y1
+ y1 = tmp
+ }
+ if (y2 > y3) {
+ tmp = y2
+ y2 = y3
+ y3 = tmp
+ }
+
+ val ry = floor(if (y0 < y2) y0 else y2)
+ val rh = ceil((if (y1 > y3) y1 else y3) - rectangle.y)
+
+ return Rectangle(rx, ry, rw, rh)
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt b/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt
new file mode 100644
index 00000000..613d3f2c
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt
@@ -0,0 +1,3 @@
+package com.icegps.math.geometry
+
+enum class MatrixMajorOrder { ROW, COLUMN }
diff --git a/math/src/main/java/com/icegps/math/geometry/Orientation.kt b/math/src/main/java/com/icegps/math/geometry/Orientation.kt
new file mode 100644
index 00000000..8af53cf4
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Orientation.kt
@@ -0,0 +1,55 @@
+package com.icegps.math.geometry
+
+import kotlin.math.*
+
+enum class Orientation(val value: Int) {
+ CLOCK_WISE(+1), COUNTER_CLOCK_WISE(-1), COLLINEAR(0);
+
+ operator fun unaryMinus(): Orientation = when (this) {
+ CLOCK_WISE -> COUNTER_CLOCK_WISE
+ COUNTER_CLOCK_WISE -> CLOCK_WISE
+ COLLINEAR -> COLLINEAR
+ }
+ operator fun unaryPlus(): Orientation = this
+
+ companion object {
+ private const val EPSILON: Double = 1e-7
+
+ //fun orient3d(v1: Vector3, v2: Vector3, v3: Vector3, epsilon: Float = EPSILONf): Orientation {
+ // // vectors from v1 to v2 and from v1 to v3
+ // val a = v2 - v1
+ // val b = v3 - v1
+ // val crossProduct = a.cross(b)
+ // // check the direction of the cross product
+ // return when {
+ // abs(crossProduct.z) < epsilon -> Orientation.COLLINEAR
+ // crossProduct.z < 0 -> Orientation.CLOCK_WISE
+ // else -> Orientation.COUNTER_CLOCK_WISE
+ // }
+ //}
+
+ internal fun checkValidUpVector(up: Vector2D) {
+ check(up.x == 0.0 && up.y.absoluteValue == 1.0) { "up vector only supports (0, -1) and (0, +1) for now" }
+ }
+
+ // @TODO: Should we provide an UP vector as reference instead? ie. Vector2(0, +1) or Vector2(0, -1), would make sense for 3d?
+ fun orient2d(pa: Point, pb: Point, pc: Point, up: Vector2D = Vector2D.UP): Orientation {
+ return orient2d(pa.x, pa.y, pb.x, pb.y, pc.x, pc.y, up = up)
+ }
+
+ fun orient2d(paX: Double, paY: Double, pbX: Double, pbY: Double, pcX: Double, pcY: Double, epsilon: Double = EPSILON, up: Vector2D = Vector2D.UP): Orientation {
+ checkValidUpVector(up)
+ // Cross product
+ val detleft: Double = (paX - pcX) * (pbY - pcY)
+ val detright: Double = (paY - pcY) * (pbX - pcX)
+ val v: Double = detleft - detright
+
+ val res: Orientation = when {
+ v.absoluteValue < epsilon -> COLLINEAR
+ v > 0 -> COUNTER_CLOCK_WISE
+ else -> CLOCK_WISE
+ }
+ return if (up.y > 0) res else -res
+ }
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Polygon.kt b/math/src/main/java/com/icegps/math/geometry/Polygon.kt
new file mode 100644
index 00000000..d94c8269
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Polygon.kt
@@ -0,0 +1,3 @@
+package com.icegps.math.geometry
+
+data class Polygon(val points: IPointList)
diff --git a/math/src/main/java/com/icegps/math/geometry/Polyline.kt b/math/src/main/java/com/icegps/math/geometry/Polyline.kt
new file mode 100644
index 00000000..0888fac9
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Polyline.kt
@@ -0,0 +1,3 @@
+package com.icegps.math.geometry
+
+data class Polyline(val points: IPointList)
diff --git a/math/src/main/java/com/icegps/math/geometry/Quaternion.kt b/math/src/main/java/com/icegps/math/geometry/Quaternion.kt
new file mode 100644
index 00000000..180bb539
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Quaternion.kt
@@ -0,0 +1,326 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.interpolation.*
+import com.icegps.math.isAlmostZero
+import kotlin.math.*
+
+// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
+//@KormaValueApi
+data class Quaternion(val x: Float, val y: Float, val z: Float, val w: Float) : IsAlmostEqualsF {
+//inline class Quaternion private constructor(val data: Float4Pack) {
+// constructor(x: Float, y: Float, z: Float, w: Float) : this(float4PackOf(x, y, z, w))
+// val x: Float get() = data.f0
+// val y: Float get() = data.f1
+// val z: Float get() = data.f2
+// val w: Float get() = data.f3
+// operator fun component1(): Float = x
+// operator fun component2(): Float = y
+// operator fun component3(): Float = z
+// operator fun component4(): Float = w
+
+ val vector: Vector4F get() = Vector4F(x, y, z, w)
+ val xyz: Vector3F get() = Vector3F(x, y, z)
+ fun conjugate() = Quaternion(-x, -y, -z, w)
+ operator fun get(index: Int): Float = when (index) {
+ 0 -> x
+ 1 -> y
+ 2 -> z
+ 3 -> w
+ else -> Float.NaN
+ }
+
+ val lengthSquared: Float get() = (x * x) + (y * y) + (z * z) + (w * w)
+ val length: Float get() = sqrt(lengthSquared)
+
+ constructor(vector: Vector4F, unit: Unit = Unit) : this(vector.x, vector.y, vector.z, vector.w)
+ constructor() : this(0f, 0f, 0f, 1f)
+ constructor(x: Double, y: Double, z: Double, w: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ fun toMatrix(): Matrix4 {
+ val v = _toMatrix()
+ return Matrix4.fromRows(
+ v[0], v[1], v[2], 0f,
+ v[3], v[4], v[5], 0f,
+ v[6], v[7], v[8], 0f,
+ 0f, 0f, 0f, 1f,
+ )
+ }
+
+ fun toMatrix3(): Matrix3 {
+ val v = _toMatrix()
+ return Matrix3.fromRows(
+ v[0], v[1], v[2],
+ v[3], v[4], v[5],
+ v[6], v[7], v[8],
+ )
+ }
+
+ private fun _toMatrix(): FloatArray {
+ val xx = x * x
+ val xy = x * y
+ val xz = x * z
+ val xw = x * w
+ val yy = y * y
+ val yz = y * z
+ val yw = y * w
+ val zz = z * z
+ val zw = z * w
+
+ return floatArrayOf(
+ 1 - 2 * (yy + zz), 2 * (xy - zw), 2 * (xz + yw),
+ 2 * (xy + zw), 1 - 2 * (xx + zz), 2 * (yz - xw),
+ 2 * (xz - yw), 2 * (yz + xw), 1 - 2 * (xx + yy),
+ )
+ }
+
+ @Deprecated("Use toMatrix instead")
+ fun toMatrixInverted(): Matrix4 = Matrix4.multiply(
+ // Left
+ w, z, -y, x,
+ -z, w, x, y,
+ y, -x, w, z,
+ -x, -y, -z, w,
+ // Right
+ w, z, -y, -x,
+ -z, w, x, -y,
+ y, -x, w, -z,
+ x, y, z, w,
+ )
+
+ operator fun unaryMinus(): Quaternion = Quaternion(-x, -y, -z, -w)
+ operator fun plus(other: Quaternion): Quaternion = Quaternion(x + other.x, y + other.y, z + other.z, w + other.w)
+ operator fun minus(other: Quaternion): Quaternion = Quaternion(x - other.x, y - other.y, z - other.z, w - other.w)
+
+ fun scaled(scale: Float): Quaternion = Quaternion.interpolated(Quaternion.IDENTITY, this, scale)
+ fun scaled(scale: Double): Quaternion = scaled(scale.toFloat())
+ fun scaled(scale: Int): Quaternion = scaled(scale.toFloat())
+
+ operator fun times(scale: Float): Quaternion = Quaternion(x * scale, y * scale, z * scale, w * scale)
+ operator fun times(scale: Double): Quaternion = times(scale.toFloat())
+ operator fun times(other: Quaternion): Quaternion {
+ val left = this
+ val right = other
+ return Quaternion(Vector4F(
+ (left.xyz * right.w) + (right.xyz * left.w) + Vector3F.cross(left.xyz, right.xyz),
+ left.w * right.w - left.xyz.dot(right.xyz)
+ ))
+ }
+
+ fun normalized(): Quaternion {
+ val length = 1f / Vector4F(x, y, z, w).length
+ return Quaternion(x / length, y / length, z / length, w / length)
+ }
+
+ /** Also known as conjugate */
+ fun inverted(): Quaternion {
+ val q = this
+ val lengthSquared = q.lengthSquared
+ if (lengthSquared.isAlmostZero()) error("Zero quaternion doesn't have invesrse")
+ val num = 1f / lengthSquared
+ return Quaternion(q.x * -num, q.y * -num, q.z * -num, q.w * num)
+ }
+
+ fun transform(v: Vector3F): Vector3F {
+ // Create a pure quaternion from the vector
+ val q = this
+ val p = Quaternion(v.x, v.y, v.z, 0f)
+ // Multiply q by p, then by the conjugate of q
+ val resultQuaternion = q * p * q.conjugate()
+ // Return the vector part of the resulting quaternion
+ return Vector3F(resultQuaternion.x, resultQuaternion.y, resultQuaternion.z)
+ }
+
+ fun toEuler(config: EulerRotation.Config = EulerRotation.Config.DEFAULT): EulerRotation = EulerRotation.fromQuaternion(this, config)
+ override fun isAlmostEquals(other: Quaternion, epsilon: Float): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon)
+ && this.y.isAlmostEquals(other.y, epsilon)
+ && this.z.isAlmostEquals(other.z, epsilon)
+ && this.w.isAlmostEquals(other.w, epsilon)
+
+ fun interpolated(other: Quaternion, t: Float): Quaternion = interpolated(this, other, t)
+ fun interpolated(other: Quaternion, t: Ratio): Quaternion = interpolated(this, other, t.toFloat())
+ fun angleTo(other: Quaternion): Angle = angleBetween(this, other)
+
+ companion object {
+ val IDENTITY = Quaternion()
+
+ fun dotProduct(l: Quaternion, r: Quaternion): Float = l.x * r.x + l.y * r.y + l.z * r.z + l.w * r.w
+
+ fun angleBetween(a: Quaternion, b: Quaternion): Angle {
+ val dot = dotProduct(a, b)
+ return Angle.arcCosine(2 * (dot * dot) - 1)
+ }
+
+ inline fun func(callback: (Int) -> Float) = Quaternion(callback(0), callback(1), callback(2), callback(3))
+ inline fun func(l: Quaternion, r: Quaternion, func: (l: Float, r: Float) -> Float) = Quaternion(
+ func(l.x, r.x),
+ func(l.y, r.y),
+ func(l.z, r.z),
+ func(l.w, r.w)
+ )
+ fun slerp(left: Quaternion, right: Quaternion, t: Float): Quaternion {
+ var tleft = left.normalized()
+ var tright = right.normalized()
+
+ var dot = Quaternion.dotProduct(tleft, right)
+
+ if (dot < 0.0f) {
+ tright = -tright
+ dot = -dot
+ }
+
+ if (dot > 0.99995f) return func(tleft, tright) { l, r -> l + t * (r - l) }
+
+ val angle0 = acos(dot)
+ val angle1 = angle0 * t
+
+ val s1 = sin(angle1) / sin(angle0)
+ val s0 = cos(angle1) - dot * s1
+
+ return func(tleft, tright) { l, r -> ((s0 * l) + (s1 * r)) }
+ }
+
+ fun nlerp(left: Quaternion, right: Quaternion, t: Double): Quaternion {
+ val sign = if (Quaternion.dotProduct(left, right) < 0) -1 else +1
+ return func { ((1f - t) * left[it] + t * right[it] * sign).toFloat() }.normalized()
+ }
+
+ fun interpolated(left: Quaternion, right: Quaternion, t: Float): Quaternion = slerp(left, right, t)
+
+ fun fromVectors(from: Vector3F, to: Vector3F): Quaternion {
+ // Normalize input vectors
+ val start = from.normalized()
+ val dest = to.normalized()
+
+ val dot = start.dot(dest)
+
+ // If vectors are opposite
+ when {
+ dot < -0.9999999f -> {
+ val tmp = Vector3F(start.y, -start.x, 0f).normalized()
+ return Quaternion(tmp.x, tmp.y, tmp.z, 0f)
+ }
+
+ dot > 0.9999999f -> {
+ // If vectors are same
+ return Quaternion()
+ }
+
+ else -> {
+ val s = kotlin.math.sqrt((1 + dot) * 2)
+ val invs = 1 / s
+
+ val c = start.cross(dest)
+
+ return Quaternion(
+ c.x * invs,
+ c.y * invs,
+ c.z * invs,
+ s * 0.5f,
+ ).normalized()
+ }
+ }
+ }
+
+ fun fromAxisAngle(axis: Vector3F, angle: Angle): Quaternion {
+ val naxis = axis.normalized()
+ val angle2 = angle / 2
+ val s = sin(angle2)
+ return Quaternion(
+ naxis.x * s,
+ naxis.y * s,
+ naxis.z * s,
+ cos(angle2)
+ )
+ }
+
+ // @TODO: Check
+ fun lookRotation(forward: Vector3F, up: Vector3F = Vector3F.UP): Quaternion {
+ //if (up == Vector3.UP) return fromVectors(Vector3.FORWARD, forward.normalized())
+ val z = forward.normalized()
+ val x = (up.normalized() cross z).normalized()
+
+ //println("x=$x, z=$z")
+ if (x.lengthSquared.isAlmostZero()) {
+ // COLLINEAR
+ return Quaternion.fromVectors(Vector3F.FORWARD, z)
+ }
+
+ val y = z cross x
+ return fromRotationMatrix(Matrix3.fromColumns(x, y, z))
+ }
+
+ fun fromRotationMatrix(m: Matrix4): Quaternion = fromRotationMatrix(
+ m.v00, m.v10, m.v20,
+ m.v01, m.v11, m.v21,
+ m.v02, m.v12, m.v22,
+ )
+
+ fun fromRotationMatrix(m: Matrix3): Quaternion = fromRotationMatrix(
+ m.v00, m.v10, m.v20,
+ m.v01, m.v11, m.v21,
+ m.v02, m.v12, m.v22,
+ )
+
+ fun fromRotationMatrix(
+ v00: Float, v10: Float, v20: Float,
+ v01: Float, v11: Float, v21: Float,
+ v02: Float, v12: Float, v22: Float,
+ ): Quaternion {
+ val t = v00 + v11 + v22
+ //println("t=$t, v00=$v00, v11=$v11, v22=$v22")
+ return when {
+ t >= 0 -> {
+ val s = .5f / sqrt(t + 1f)
+ //println("[0]")
+ Quaternion(((v21 - v12) * s), ((v02 - v20) * s), ((v10 - v01) * s), (0.25f / s))
+ }
+ v00 > v11 && v00 > v22 -> {
+ val s = 2f * sqrt(1f + v00 - v11 - v22)
+ //println("[1]")
+ Quaternion((0.25f * s), ((v01 + v10) / s), ((v02 + v20) / s), ((v21 - v12) / s))
+ }
+ v11 > v22 -> {
+ val s = 2f * sqrt(1f + v11 - v00 - v22)
+ //println("[2]")
+ Quaternion(((v01 + v10) / s), (.25f * s), ((v12 + v21) / s), ((v02 - v20) / s))
+ }
+ else -> {
+ val s = 2f * sqrt(1f + v22 - v00 - v11)
+ //println("[3]")
+ Quaternion(((v02 + v20) / s), ((v12 + v21) / s), (.25f * s), ((v10 - v01) / s))
+ }
+ }
+ }
+
+ fun fromEuler(e: EulerRotation): Quaternion = e.toQuaternion()
+ fun fromEuler(roll: Angle, pitch: Angle, yaw: Angle): Quaternion = EulerRotation(roll, pitch, yaw).toQuaternion()
+
+ fun toEuler(x: Float, y: Float, z: Float, w: Float, config: EulerRotation.Config = EulerRotation.Config.DEFAULT): EulerRotation {
+ return EulerRotation.Companion.fromQuaternion(x, y, z, w, config)
+ /*
+ val t = y * x + z * w
+ // Gimbal lock, if any: positive (+1) for north pole, negative (-1) for south pole, zero (0) when no gimbal lock
+ val pole = if (t > 0.499f) 1 else if (t < -0.499f) -1 else 0
+ return EulerRotation(
+ roll = when (pole) {
+ 0 -> Angle.asin((2f * (w * x - z * y)).clamp(-1f, +1f))
+ else -> (pole.toFloat() * PIF * .5f).radians
+ },
+ pitch = when (pole) {
+ 0 -> Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x))
+ else -> Angle.ZERO
+ },
+ yaw = when (pole) {
+ 0 -> Angle.atan2(2f * (w * z + y * x), 1f - 2f * (x * x + z * z))
+ else -> Angle.atan2(y, w) * pole.toFloat() * 2f
+ },
+ )
+
+ */
+ }
+ }
+}
+
+fun Angle.Companion.between(a: Quaternion, b: Quaternion): Angle = Quaternion.angleBetween(a, b)
diff --git a/math/src/main/java/com/icegps/math/geometry/Ray.kt b/math/src/main/java/com/icegps/math/geometry/Ray.kt
new file mode 100644
index 00000000..81b25871
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Ray.kt
@@ -0,0 +1,90 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.annotations.*
+
+typealias Ray = Ray2D
+typealias Ray2 = Ray
+
+/** Represents an infinite [Ray] starting at [point] in the specified [direction] with an [angle] */
+//inline class Ray(val data: Float4Pack) {
+data class Ray2D
+/** Constructs a [Ray] starting from [point] in the specified [direction] */
+private constructor(
+ /** Starting point */
+ val point: Point,
+ /** Normalized direction of the ray starting at [point] */
+ val direction: Vector2D,
+) : IsAlmostEquals {
+ companion object {
+ /** Creates a ray starting in [start] and passing by [end] */
+ fun fromTwoPoints(start: Point, end: Point): Ray = Ray(start, end - start, Unit)
+ }
+
+ //val point: Point get() = Point(data.f0, data.f1)
+ //val direction: Vector2 get() = Vector2(data.f2, data.f3)
+ /** Angle between two points */
+ val angle: Angle get() = direction.angle
+
+ /** Constructs a [Ray] starting from [point] in the specified [direction] */
+ constructor(point: Point, direction: Vector2D, unit: Unit = Unit) : this(point, direction.normalized)
+ /** Constructs a [Ray] starting from [point] in the specified [angle] */
+ constructor(point: Point, angle: Angle) : this(point, Vector2D.polar(angle), Unit)
+
+ //private constructor(point: Point, normalizedDirection: Vector2, unit: Unit) : this(point.x, point.y, normalizedDirection.x, normalizedDirection.y)
+
+ /** Checks if [this] and [other]are equals with an [epsilon] difference */
+ override fun isAlmostEquals(other: Ray, epsilon: Double): Boolean =
+ this.point.isAlmostEquals(other.point, epsilon) && this.direction.isAlmostEquals(other.direction, epsilon)
+
+ /** Checks if [this] and [other]are equals with an [epsilon] tolerance */
+ fun transformed(m: Matrix): Ray = Ray(m.transform(point), m.deltaTransform(direction).normalized)
+
+ /** Converts this [Ray] into a [Line] of a specific [length] starting by [point] */
+ fun toLine(length: Double = 100000.0): Line = Line(point, point + direction * length)
+
+ override fun toString(): String = "Ray($point, $angle)"
+}
+
+typealias Ray3 = Ray3F
+
+data class Ray3F(val pos: Vector3F, val dir: Vector3F) {//: Shape3D {
+ //override val center: Vector3 get() = pos
+ //override val volume: Float = 0f
+}
+
+@KormaMutableApi
+fun Ray3F.intersectRayAABox1(box: AABB3D) : Boolean {
+ val ray = this
+ // r.dir is unit direction vector of ray
+ val dirfrac = ray.dir.inv()
+ // lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
+ // r.org is origin of ray
+ val t1 = (box.min.x - ray.pos.x) * dirfrac.x
+ val t2 = (box.max.x - ray.pos.x) * dirfrac.x
+ val t3 = (box.min.y - ray.pos.y) * dirfrac.y
+ val t4 = (box.max.y - ray.pos.y) * dirfrac.y
+ val t5 = (box.min.z - ray.pos.z) * dirfrac.z
+ val t6 = (box.max.z - ray.pos.z) * dirfrac.z
+
+ val tmin =
+ kotlin.math.max(kotlin.math.max(kotlin.math.min(t1, t2), kotlin.math.min(t3, t4)), kotlin.math.min(t5, t6))
+ val tmax =
+ kotlin.math.min(kotlin.math.min(kotlin.math.max(t1, t2), kotlin.math.max(t3, t4)), kotlin.math.max(t5, t6))
+
+ // if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us
+ if (tmax < 0) {
+ val t = tmax
+ return false
+ }
+
+ // if tmin > tmax, ray doesn't intersect AABB
+ if (tmin > tmax) {
+ val t = tmax
+ return false
+ }
+
+ val t = tmin
+ return true
+
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/RectCorners.kt b/math/src/main/java/com/icegps/math/geometry/RectCorners.kt
new file mode 100644
index 00000000..e4ca9bb1
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/RectCorners.kt
@@ -0,0 +1,28 @@
+package com.icegps.math.geometry
+
+data class RectCorners(
+ val topLeft: Double,
+ val topRight: Double,
+ val bottomRight: Double,
+ val bottomLeft: Double,
+) {
+ operator fun unaryMinus(): RectCorners = this * (-1.0)
+ operator fun unaryPlus(): RectCorners = this
+ operator fun plus(that: RectCorners): RectCorners = RectCorners(this.topLeft + that.topLeft, this.topRight + that.topRight, this.bottomLeft + that.bottomLeft, this.bottomRight + that.bottomRight)
+ operator fun minus(that: RectCorners): RectCorners = RectCorners(this.topLeft - that.topLeft, this.topRight - that.topRight, this.bottomLeft - that.bottomLeft, this.bottomRight - that.bottomRight)
+ operator fun times(scale: Double): RectCorners = RectCorners(topLeft * scale, topRight * scale, bottomRight * scale, bottomLeft * scale)
+ operator fun div(scale: Double): RectCorners = this * (1.0 / scale)
+
+ companion object {
+ val EMPTY = RectCorners(0)
+ val ZERO = RectCorners(0)
+ val ONE = RectCorners(1.0)
+ val MINUS_ONE = RectCorners(-1.0)
+ val NaN = RectCorners(Double.NaN)
+
+ inline operator fun invoke(corner: Number): RectCorners = RectCorners(corner.toDouble(), corner.toDouble(), corner.toDouble(), corner.toDouble())
+ inline operator fun invoke(topLeftBottomRight: Number, topRightAndBottomLeft: Number): RectCorners = RectCorners(topLeftBottomRight.toDouble(), topRightAndBottomLeft.toDouble(), topLeftBottomRight.toDouble(), topRightAndBottomLeft.toDouble())
+ inline operator fun invoke(topLeft: Number, topRightAndBottomLeft: Number, bottomRight: Number): RectCorners = RectCorners(topLeft.toDouble(), topRightAndBottomLeft.toDouble(), bottomRight.toDouble(), topRightAndBottomLeft.toDouble())
+ inline operator fun invoke(topLeft: Number, topRight: Number, bottomRight: Number, bottomLeft: Number): RectCorners = RectCorners(topLeft.toDouble(), topRight.toDouble(), bottomRight.toDouble(), bottomLeft.toDouble())
+ }
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Rectangle.kt b/math/src/main/java/com/icegps/math/geometry/Rectangle.kt
new file mode 100644
index 00000000..260ccf08
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Rectangle.kt
@@ -0,0 +1,291 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.geometry.shape.*
+import com.icegps.math.interpolation.*
+import com.icegps.number.*
+import kotlin.math.*
+
+typealias RectangleD = Rectangle
+
+//@KormaValueApi
+//inline class Rectangle(val data: Float4Pack) : Shape2D, Interpolable {
+//inline class Rectangle(val data: Float4) : Shape2D {
+data class Rectangle(val x: Double, val y: Double, val width: Double, val height: Double) : SimpleShape2D, IsAlmostEquals {
+ val int: RectangleInt get() = toInt()
+
+ //operator fun component1(): Float = x
+ //operator fun component2(): Float = y
+ //operator fun component3(): Float = width
+ //operator fun component4(): Float = height
+ //val x: Float get() = data.f0
+ //val y: Float get() = data.f1
+ //val width: Float get() = data.f2
+ //val height: Float get() = data.f3
+ //fun copy(x: Float = this.x, y: Float = this.y, width: Float = this.width, height: Float = this.height): Rectangle = Rectangle(x, y, width, height)
+
+ @Deprecated("", ReplaceWith("this")) fun clone(): Rectangle = this
+ @Deprecated("", ReplaceWith("this")) val immutable: Rectangle get() = this
+
+ val position: Point get() = Point(x, y)
+ val size: Size get() = Size(width, height)
+
+ val isZero: Boolean get() = this == ZERO
+ val isInfinite: Boolean get() = this == INFINITE
+ //val isNaN: Boolean get() = this == NaN
+ val isNaN: Boolean get() = this.x.isNaN()
+ val isNIL: Boolean get() = isNaN
+ val isNotNIL: Boolean get() = !isNIL
+
+ override fun isAlmostEquals(other: Rectangle, epsilon: Double): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon) &&
+ this.y.isAlmostEquals(other.y, epsilon) &&
+ this.width.isAlmostEquals(other.width, epsilon) &&
+ this.height.isAlmostEquals(other.height, epsilon)
+
+ fun toStringBounds(): String = "Rectangle([${left.niceStr},${top.niceStr}]-[${right.niceStr},${bottom.niceStr}])"
+ fun toStringSize(): String = "Rectangle([${left.niceStr},${top.niceStr}],[${width.niceStr},${height.niceStr}])"
+ fun toStringCompat(): String = "Rectangle(x=${left.niceStr}, y=${top.niceStr}, w=${width.niceStr}, h=${height.niceStr})"
+
+ //override fun interpolateWith(ratio: Ratio, other: Rectangle): Rectangle = interpolated(this, other, ratio)
+
+ override fun toString(): String = when {
+ isNIL -> "null"
+ else -> "Rectangle(x=${x.niceStr}, y=${y.niceStr}, width=${width.niceStr}, height=${height.niceStr})"
+ }
+
+ companion object {
+ val ZERO = Rectangle(0, 0, 0, 0)
+ val INFINITE = Rectangle(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
+ val NaN = Rectangle(Float.NaN, Float.NaN, 0f, 0f)
+ val NIL get() = NaN
+
+ operator fun invoke(): Rectangle = ZERO
+ operator fun invoke(p: Point, s: Size): Rectangle = Rectangle(p.x, p.y, s.width, s.height)
+ operator fun invoke(x: Int, y: Int, width: Int, height: Int): Rectangle = Rectangle(Point(x, y), Size(width, height))
+ operator fun invoke(x: Float, y: Float, width: Float, height: Float): Rectangle = Rectangle(Point(x, y), Size(width, height))
+ operator fun invoke(x: Double, y: Double, width: Double, height: Double): Rectangle = Rectangle(Point(x, y), Size(width, height))
+ inline operator fun invoke(x: Number, y: Number, width: Number, height: Number): Rectangle = Rectangle(Point(x, y), Size(width, height))
+
+ fun fromBounds(left: Double, top: Double, right: Double, bottom: Double): Rectangle = Rectangle(left, top, right - left, bottom - top)
+ fun fromBounds(left: Int, top: Int, right: Int, bottom: Int): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble())
+ fun fromBounds(left: Float, top: Float, right: Float, bottom: Float): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble())
+ fun fromBounds(point1: Point, point2: Point): Rectangle = Rectangle(point1, (point2 - point1).toSize())
+ inline fun fromBounds(left: Number, top: Number, right: Number, bottom: Number): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble())
+
+ fun isContainedIn(a: Rectangle, b: Rectangle): Boolean = a.x >= b.x && a.y >= b.y && a.x + a.width <= b.x + b.width && a.y + a.height <= b.y + b.height
+
+ fun interpolated(a: Rectangle, b: Rectangle, ratio: Ratio): Rectangle = Rectangle.fromBounds(
+ ratio.interpolate(a.left, b.left),
+ ratio.interpolate(a.top, b.top),
+ ratio.interpolate(a.right, b.right),
+ ratio.interpolate(a.bottom, b.bottom),
+ )
+ }
+
+ operator fun times(scale: Double): Rectangle = Rectangle(x * scale, y * scale, width * scale, height * scale)
+ operator fun times(scale: Float): Rectangle = times(scale.toDouble())
+ operator fun times(scale: Int): Rectangle = times(scale.toDouble())
+
+ operator fun div(scale: Double): Rectangle = Rectangle(x / scale, y / scale, width / scale, height / scale)
+ operator fun div(scale: Float): Rectangle = div(scale.toDouble())
+ operator fun div(scale: Int): Rectangle = div(scale.toDouble())
+
+ operator fun contains(that: Point): Boolean = contains(that.x, that.y)
+ operator fun contains(that: Vector2F): Boolean = contains(that.x, that.y)
+ operator fun contains(that: Vector2I): Boolean = contains(that.x, that.y)
+ fun contains(x: Double, y: Double): Boolean = (x >= left && x < right) && (y >= top && y < bottom)
+ fun contains(x: Float, y: Float): Boolean = contains(x.toDouble(), y.toDouble())
+ fun contains(x: Int, y: Int): Boolean = contains(x.toDouble(), y.toDouble())
+
+ override val area: Double get() = width * height
+ override val perimeter: Double get() = (width + height) * 2
+ override val closed: Boolean = true
+
+ override fun containsPoint(p: Point): Boolean = (p.x >= left && p.x < right) && (p.y >= top && p.y < bottom)
+ override fun getBounds(): Rectangle = this
+
+ override fun distance(p: Point): Double {
+ val p = p - center
+ val b = Vector2D(width * 0.5, height * 0.5)
+ val d = p.absoluteValue - b
+ return max(d, Vector2D.ZERO).length + min(max(d.x, d.y), 0.0)
+ }
+
+ override fun normalVectorAt(p: Point): Vector2D {
+ val pp = projectedPoint(p)
+ val x = when (pp.x) {
+ left -> -1.0
+ right -> +1.0
+ else -> 0.0
+ }
+ val y = when (pp.y) {
+ top -> -1.0
+ bottom -> +1.0
+ else -> 0.0
+ }
+ return Point(x, y).normalized
+ }
+
+ override fun projectedPoint(p: Point): Point {
+ val p0 = Line(topLeft, topRight).projectedPoint(p)
+ val p1 = Line(topRight, bottomRight).projectedPoint(p)
+ val p2 = Line(bottomRight, bottomLeft).projectedPoint(p)
+ val p3 = Line(bottomLeft, topLeft).projectedPoint(p)
+ val d0 = (p0 - p).lengthSquared
+ val d1 = (p1 - p).lengthSquared
+ val d2 = (p2 - p).lengthSquared
+ val d3 = (p3 - p).lengthSquared
+ val dmin = com.icegps.math.min(d0, d1, d2, d3)
+ return when (dmin) {
+ d0 -> p0
+ d1 -> p1
+ d2 -> p2
+ d3 -> p3
+ else -> p0
+ }
+
+ //val px = p.x.clamp(left, right)
+ //val py = p.y.clamp(top, bottom)
+ //val distTop = (py - top).absoluteValue
+ //val distBottom = (py - bottom).absoluteValue
+ //val minDistY = min(distTop, distBottom)
+ //val distLeft = (px - left).absoluteValue
+ //val distRight = (px - right).absoluteValue
+ //val minDistX = min(distLeft, distRight)
+ //if (minDistX < minDistY) {
+ // return Point(if (distLeft < distRight) left else right, py)
+ //} else {
+ // return Point(px, if (distTop < distBottom) top else bottom)
+ //}
+ }
+
+ val isEmpty: Boolean get() = width == 0.0 && height == 0.0
+ val isNotEmpty: Boolean get() = !isEmpty
+
+ val left: Double get() = x
+ val top: Double get() = y
+ val right: Double get() = x + width
+ val bottom: Double get() = y + height
+
+ val topLeft: Point get() = Point(left, top)
+ val topRight: Point get() = Point(right, top)
+ val bottomLeft: Point get() = Point(left, bottom)
+ val bottomRight: Point get() = Point(right, bottom)
+
+ val centerX: Double get() = (right + left) * 0.5
+ val centerY: Double get() = (bottom + top) * 0.5
+ override val center: Point get() = Point(centerX, centerY)
+
+ fun without(padding: Margin): Rectangle = fromBounds(
+ left + padding.left,
+ top + padding.top,
+ right - padding.right,
+ bottom - padding.bottom
+ )
+
+ fun with(margin: Margin): Rectangle = fromBounds(
+ left - margin.left,
+ top - margin.top,
+ right + margin.right,
+ bottom + margin.bottom
+ )
+
+ infix fun intersects(that: Rectangle): Boolean = intersectsX(that) && intersectsY(that)
+ infix fun intersectsX(that: Rectangle): Boolean = that.left <= this.right && that.right >= this.left
+ infix fun intersectsY(that: Rectangle): Boolean = that.top <= this.bottom && that.bottom >= this.top
+
+ infix fun intersectionOrNull(that: Rectangle): Rectangle? = if (this intersects that) Rectangle(
+ max(this.left, that.left), max(this.top, that.top),
+ min(this.right, that.right), min(this.bottom, that.bottom)
+ ) else null
+
+ infix fun intersection(that: Rectangle): Rectangle = if (this intersects that) Rectangle(
+ max(this.left, that.left), max(this.top, that.top),
+ min(this.right, that.right), min(this.bottom, that.bottom)
+ ) else Rectangle.NIL
+
+ fun toInt(): RectangleInt = RectangleInt(x.toInt(), y.toInt(), width.toInt(), height.toInt())
+ fun toIntRound(): RectangleInt = RectangleInt(x.toIntRound(), y.toIntRound(), width.toIntRound(), height.toIntRound())
+ fun toIntCeil(): RectangleInt = RectangleInt(x.toIntCeil(), y.toIntCeil(), width.toIntCeil(), height.toIntCeil())
+ fun toIntFloor(): RectangleInt = RectangleInt(x.toIntFloor(), y.toIntFloor(), width.toIntFloor(), height.toIntFloor())
+
+ fun getAnchoredPoint(anchor: Anchor): Point = Point(left + width * anchor.sx, top + height * anchor.sy)
+
+ fun expanded(border: MarginInt): Rectangle =
+ fromBounds(left - border.left, top - border.top, right + border.right, bottom + border.bottom)
+
+ fun copyBounds(left: Double = this.left, top: Double = this.top, right: Double = this.right, bottom: Double = this.bottom): Rectangle =
+ Rectangle.fromBounds(left, top, right, bottom)
+
+ fun translated(delta: Point): Rectangle = copy(x = this.x + delta.x, y = this.y + delta.y)
+
+ fun transformed(m: Matrix): Rectangle {
+ val tl = m.transform(topLeft)
+ val tr = m.transform(topRight)
+ val bl = m.transform(bottomLeft)
+ val br = m.transform(bottomRight)
+ val min = Point.minComponents(tl, tr, bl, br)
+ val max = Point.maxComponents(tl, tr, bl, br)
+ return Rectangle.fromBounds(min, max)
+ }
+
+ fun normalized(): Rectangle =
+ Rectangle.fromBounds(Point.minComponents(topLeft, bottomRight), Point.maxComponents(topLeft, bottomRight))
+
+ fun roundDecimalPlaces(places: Int): Rectangle = Rectangle(
+ x.roundDecimalPlaces(places),
+ y.roundDecimalPlaces(places),
+ width.roundDecimalPlaces(places),
+ height.roundDecimalPlaces(places)
+ )
+
+ fun rounded(): Rectangle = Rectangle(round(x), round(y), round(width), round(height))
+ fun floored(): Rectangle = Rectangle(floor(x), floor(y), floor(width), floor(height))
+ fun ceiled(): Rectangle = Rectangle(ceil(x), ceil(y), ceil(width), ceil(height))
+}
+
+
+fun Iterable.bounds(): Rectangle {
+ var first = true
+ var left = 0.0
+ var right = 0.0
+ var top = 0.0
+ var bottom = 0.0
+ for (r in this) {
+ if (first) {
+ left = r.left
+ right = r.right
+ top = r.top
+ bottom = r.bottom
+ first = false
+ } else {
+ left = min(left, r.left)
+ right = max(right, r.right)
+ top = min(top, r.top)
+ bottom = max(bottom, r.bottom)
+ }
+ }
+ return Rectangle.fromBounds(left, top, right, bottom)
+}
+
+/**
+ * Circle that touches or contains all the corners ([Rectangle.topLeft], [Rectangle.topRight], [Rectangle.bottomLeft], [Rectangle.bottomRight]) of the rectangle.
+ */
+fun Rectangle.outerCircle(): Circle {
+ val centerX = centerX
+ val centerY = centerY
+ return Circle(center, Point.distance(centerX, centerY, right, top))
+}
+
+fun Rectangle.place(item: Size, anchor: Anchor, scale: ScaleMode): Rectangle {
+ val outSize = scale(item, this.size)
+ val p = (this.size - outSize) * anchor
+ return Rectangle(p, outSize)
+}
+
+//fun RectangleInt.place(item: SizeInt, anchor: Anchor, scale: ScaleMode): RectangleInt {
+// val outSize = scale(item, this.size)
+// val p = (this.size - outSize) * anchor
+// return RectangleInt(p, outSize)
+//}
diff --git a/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt b/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt
new file mode 100644
index 00000000..03d07747
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt
@@ -0,0 +1,89 @@
+package com.icegps.math.geometry
+
+typealias RectangleI = RectangleInt
+
+//@KormaValueApi
+data class RectangleInt(
+ val x: Int, val y: Int,
+ val width: Int, val height: Int,
+) {
+ constructor() : this(0, 0, 0, 0)
+
+ val position: Vector2I get() = Vector2I(x, y)
+
+ val area: Int get() = width * height
+ val isEmpty: Boolean get() = width == 0 && height == 0
+ val isNotEmpty: Boolean get() = !isEmpty
+
+ val left: Int get() = x
+ val top: Int get() = y
+ val right: Int get() = x + width
+ val bottom: Int get() = y + height
+
+ val topLeft: Vector2I get() = Vector2I(left, top)
+ val topRight: Vector2I get() = Vector2I(right, top)
+ val bottomLeft: Vector2I get() = Vector2I(left, bottom)
+ val bottomRight: Vector2I get() = Vector2I(right, bottom)
+
+ val centerX: Int get() = ((right + left) * 0.5f).toInt()
+ val centerY: Int get() = ((bottom + top) * 0.5f).toInt()
+ val center: Vector2I get() = Vector2I(centerX, centerY)
+
+ operator fun times(scale: Double): RectangleInt = RectangleInt(
+ (x * scale).toInt(), (y * scale).toInt(),
+ (width * scale).toInt(), (height * scale).toInt()
+ )
+ operator fun times(scale: Float): RectangleInt = this * scale.toDouble()
+ operator fun times(scale: Int): RectangleInt = this * scale.toDouble()
+
+ operator fun div(scale: Float): RectangleInt = RectangleInt(
+ (x / scale).toInt(), (y / scale).toInt(),
+ (width / scale).toInt(), (height / scale).toInt()
+ )
+
+ operator fun div(scale: Double): RectangleInt = this / scale.toFloat()
+ operator fun div(scale: Int): RectangleInt = this / scale.toFloat()
+
+ operator fun contains(that: Point): Boolean = contains(that.x, that.y)
+ operator fun contains(that: Vector2I): Boolean = contains(that.x, that.y)
+ fun contains(x: Float, y: Float): Boolean = (x >= left && x < right) && (y >= top && y < bottom)
+ fun contains(x: Double, y: Double): Boolean = contains(x.toFloat(), y.toFloat())
+ fun contains(x: Int, y: Int): Boolean = contains(x.toFloat(), y.toFloat())
+
+ fun sliceWithBounds(left: Int, top: Int, right: Int, bottom: Int, clamped: Boolean = true): RectangleInt {
+ val left = if (!clamped) left else left.coerceIn(0, this.width)
+ val right = if (!clamped) right else right.coerceIn(0, this.width)
+ val top = if (!clamped) top else top.coerceIn(0, this.height)
+ val bottom = if (!clamped) bottom else bottom.coerceIn(0, this.height)
+ return fromBounds(this.x + left, this.y + top, this.x + right, this.y + bottom)
+ }
+
+ fun sliceWithSize(x: Int, y: Int, width: Int, height: Int, clamped: Boolean = true): RectangleInt =
+ sliceWithBounds(x, y, x + width, y + height, clamped)
+
+ override fun toString(): String = "Rectangle(x=${x}, y=${y}, width=${width}, height=${height})"
+
+ companion object {
+ fun union(a: RectangleInt, b: RectangleInt): RectangleInt = fromBounds(
+ kotlin.math.min(a.left, b.left),
+ kotlin.math.min(a.top, b.top),
+ kotlin.math.max(a.right, b.right),
+ kotlin.math.max(a.bottom, b.bottom)
+ )
+
+ fun fromBounds(topLeft: Vector2I, bottomRight: Vector2I): RectangleInt {
+ val size = (bottomRight - topLeft)
+ return RectangleInt(topLeft.x, topLeft.y, size.x, size.y)
+ }
+ fun fromBounds(left: Int, top: Int, right: Int, bottom: Int): RectangleInt = fromBounds(Vector2I(left, top), Vector2I(right, bottom))
+
+ operator fun invoke(position: PointInt, size: SizeInt): RectangleInt = RectangleInt(position.x, position.y, size.width, size.height)
+ }
+
+ val float: Rectangle get() = Rectangle(x, y, width, height)
+ val size: SizeInt get() = SizeInt(width, height)
+ fun toFloat(): Rectangle = Rectangle(position.toDouble(), size.toDouble())
+ fun expanded(border: MarginInt): RectangleInt =
+ RectangleInt.fromBounds(left - border.left, top - border.top, right + border.right, bottom + border.bottom)
+
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt b/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt
new file mode 100644
index 00000000..0ccb586a
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt
@@ -0,0 +1,18 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.interpolation.*
+
+data class RoundRectangle(val rect: Rectangle, val corners: RectCorners) {
+ companion object {
+ private fun areaQuarter(radius: Double): Double = Arc_length(radius, Angle.QUARTER)
+ private fun areaComplementaryQuarter(radius: Double): Double = (radius * radius) - areaQuarter(radius)
+ private fun Arc_length(radius: Double, angle: Angle): Double = PI2 * radius * angle.ratio
+ }
+
+ val area: Double get() = rect.area - (
+ areaComplementaryQuarter(corners.topLeft) +
+ areaComplementaryQuarter(corners.topRight) +
+ areaComplementaryQuarter(corners.bottomLeft) +
+ areaComplementaryQuarter(corners.bottomRight)
+ )
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Scale.kt b/math/src/main/java/com/icegps/math/geometry/Scale.kt
new file mode 100644
index 00000000..16fa1a33
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Scale.kt
@@ -0,0 +1,52 @@
+package com.icegps.math.geometry
+
+//@KormaValueApi
+//inline class Scale internal constructor(internal val raw: Float2Pack) {
+data class Scale(val scaleX: Double, val scaleY: Double) {
+ companion object {
+ val IDENTITY = Scale(1f, 1f)
+ }
+
+ //val scaleX: Float get() = raw.f0
+ //val scaleY: Float get() = raw.f1
+ val scaleAvg: Double get() = scaleX * .5 + scaleY * .5
+
+ @Deprecated("", ReplaceWith("scaleAvg"))
+ val avg: Double get() = scaleAvg
+
+ constructor() : this(1f, 1f)
+ constructor(scale: Float) : this(scale, scale)
+ constructor(scale: Double) : this(scale, scale)
+ constructor(scale: Int) : this(scale.toDouble())
+ //constructor(scaleX: Float, scaleY: Float) : this(float2PackOf(scaleX, scaleY))
+ constructor(scaleX: Float, scaleY: Float) : this(scaleX.toDouble(), scaleY.toDouble())
+ constructor(scaleX: Int, scaleY: Int) : this(scaleX.toDouble(), scaleY.toDouble())
+
+ operator fun unaryMinus(): Scale = Scale(-scaleX, -scaleY)
+ operator fun unaryPlus(): Scale = this
+
+ operator fun plus(other: Scale): Scale = Scale(scaleX + other.scaleX, scaleY + other.scaleY)
+ operator fun minus(other: Scale): Scale = Scale(scaleX - other.scaleX, scaleY - other.scaleY)
+
+ operator fun times(other: Scale): Scale = Scale(scaleX * other.scaleX, scaleY * other.scaleY)
+ operator fun times(other: Float): Scale = Scale(scaleX * other, scaleY * other)
+ operator fun div(other: Scale): Scale = Scale(scaleX / other.scaleX, scaleY / other.scaleY)
+ operator fun div(other: Float): Scale = Scale(scaleX / other, scaleY / other)
+ operator fun rem(other: Scale): Scale = Scale(scaleX % other.scaleX, scaleY % other.scaleY)
+ operator fun rem(other: Float): Scale = Scale(scaleX % other, scaleY % scaleY)
+}
+
+operator fun Vector2D.times(other: Scale): Vector2D = Vector2D(x * other.scaleX, y * other.scaleY)
+operator fun Vector2D.div(other: Scale): Vector2D = Vector2D(x / other.scaleX, y / other.scaleY)
+operator fun Vector2D.rem(other: Scale): Vector2D = Vector2D(x % other.scaleX, y % other.scaleY)
+
+operator fun Vector2F.times(other: Scale): Vector2F = Vector2F(x * other.scaleX, y * other.scaleY)
+operator fun Vector2F.div(other: Scale): Vector2F = Vector2F(x / other.scaleX, y / other.scaleY)
+operator fun Vector2F.rem(other: Scale): Vector2F = Vector2F(x % other.scaleX, y % other.scaleY)
+
+fun Vector2F.toScale(): Scale = Scale(x, y)
+fun Vector2D.toScale(): Scale = Scale(x, y)
+
+fun Scale.toPoint(): Point = Point(scaleX, scaleY)
+fun Scale.toVector2(): Vector2D = Vector2D(scaleX, scaleY)
+fun Scale.toVector2F(): Vector2F = Vector2F(scaleX, scaleY)
diff --git a/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt b/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt
new file mode 100644
index 00000000..28ae8558
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt
@@ -0,0 +1,40 @@
+package com.icegps.math.geometry
+
+class ScaleMode(
+ val name: String? = null,
+ val transform: (item: Size, container: Size) -> Size
+) {
+ override fun toString(): String = "ScaleMode($name)"
+
+ operator fun invoke(item: Size, container: Size): Size = transform(item, container)
+ operator fun invoke(item: SizeInt, container: SizeInt): SizeInt = transform(item.toFloat(), container.toFloat()).toInt()
+
+ companion object {
+ val COVER: ScaleMode = ScaleMode("COVER") { i, c -> i * (c / i).toVector2().maxComponent() }
+ val SHOW_ALL: ScaleMode = ScaleMode("SHOW_ALL") { i, c -> i * (c / i).toVector2().minComponent() }
+ val FIT: ScaleMode get() = SHOW_ALL
+ val FILL: ScaleMode get() = EXACT
+ val EXACT: ScaleMode = ScaleMode("EXACT") { i, c -> c }
+ val NO_SCALE: ScaleMode = ScaleMode("NO_SCALE") { i, c -> i }
+ }
+}
+
+fun Rectangle.applyScaleMode(
+ container: Rectangle, mode: ScaleMode, anchor: Anchor
+): Rectangle = this.size.applyScaleMode(container, mode, anchor)
+
+fun SizeInt.applyScaleMode(container: RectangleInt, mode: ScaleMode, anchor: Anchor): RectangleInt = this.toFloat().applyScaleMode(container.toFloat(), mode, anchor).toInt()
+fun SizeInt.applyScaleMode(container: SizeInt, mode: ScaleMode): SizeInt = mode(this, container)
+fun SizeInt.fitTo(container: SizeInt): SizeInt = applyScaleMode(container, ScaleMode.SHOW_ALL)
+
+fun Size.applyScaleMode(container: Rectangle, mode: ScaleMode, anchor: Anchor): Rectangle {
+ val outSize = this.applyScaleMode(container.size, mode)
+ return Rectangle(
+ (container.x + anchor.sx * (container.width - outSize.width)),
+ (container.y + anchor.sy * (container.height - outSize.height)),
+ outSize.width,
+ outSize.height
+ )
+}
+fun Size.applyScaleMode(container: Size, mode: ScaleMode): Size = mode(this, container)
+fun Size.fitTo(container: Size): Size = applyScaleMode(container, ScaleMode.SHOW_ALL)
diff --git a/math/src/main/java/com/icegps/math/geometry/Size.kt b/math/src/main/java/com/icegps/math/geometry/Size.kt
new file mode 100644
index 00000000..56a1dc8b
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Size.kt
@@ -0,0 +1,150 @@
+package com.icegps.math.geometry
+
+import com.icegps.number.*
+import kotlin.math.*
+
+typealias Size = Size2D
+typealias Size3 = Size2F
+
+data class Size2F(val width: Float, val height: Float)
+data class Size3F(val width: Float, val height: Float, val depth: Float)
+data class Size3D(val width: Double, val height: Double, val depth: Double)
+
+/**
+ * A class representing a size with a [width] and a [height] as Float.
+ */
+data class Size2D(val width: Double, val height: Double) {//: Sizeable {
+ companion object {
+ inline operator fun invoke(width: Number, height: Number): Size2D = Size2D(width.toDouble(), height.toDouble())
+ val ZERO = Size(0.0, 0.0)
+ fun square(value: Int): Size = Size(value, value)
+ fun square(value: Double): Size = Size(value, value)
+ }
+
+ fun isEmpty(): Boolean = width == 0.0 || height == 0.0
+
+ fun avgComponent(): Double = width * 0.5 + height * 0.5
+ fun minComponent(): Double = min(width, height)
+ fun maxComponent(): Double = max(width, height)
+
+ val area: Double get() = width * height
+ val perimeter: Double get() = width * 2 + height * 2
+
+ //(val width: Double, val height: Double) {
+ constructor() : this(0.0, 0.0)
+ constructor(width: Float, height: Float) : this(width.toDouble(), height.toDouble())
+ constructor(width: Int, height: Int) : this(width.toDouble(), height.toDouble())
+
+ operator fun unaryMinus(): Size = Size(-width, -height)
+ operator fun unaryPlus(): Size = this
+
+ operator fun minus(other: Size): Size = Size(width - other.width, height - other.height)
+ operator fun plus(other: Size): Size = Size(width + other.width, height + other.height)
+ operator fun times(scale: Scale): Size = Size(width * scale.scaleX, height * scale.scaleY)
+ operator fun times(scale: Vector2F): Size = Size(width * scale.x, height * scale.y)
+ operator fun times(s: Float): Size = Size(width * s, height * s)
+ operator fun times(s: Double): Size = times(s.toFloat())
+ operator fun times(s: Int): Size = times(s.toFloat())
+ operator fun div(other: Size): Scale = Scale(width / other.width, height / other.height)
+ operator fun div(s: Float): Size = Size(width / s, height / s)
+ operator fun div(s: Double): Size = div(s.toFloat())
+ operator fun div(s: Int): Size = div(s.toFloat())
+
+ //override val size: Size get() = this
+
+ override fun toString(): String = "Size(width=${width.niceStr}, height=${height.niceStr})"
+}
+
+operator fun Vector2D.plus(other: Size): Vector2D = Vector2D(x + other.width, y + other.height)
+operator fun Vector2D.minus(other: Size): Vector2D = Vector2D(x - other.width, y - other.height)
+operator fun Vector2D.times(other: Size): Vector2D = Vector2D(x * other.width, y * other.height)
+operator fun Vector2D.div(other: Size): Vector2D = Vector2D(x / other.width, y / other.height)
+operator fun Vector2D.rem(other: Size): Vector2D = Vector2D(x % other.width, y % other.height)
+
+operator fun Vector2F.plus(other: Size): Vector2F = Vector2F(x + other.width, y + other.height)
+operator fun Vector2F.minus(other: Size): Vector2F = Vector2F(x - other.width, y - other.height)
+operator fun Vector2F.times(other: Size): Vector2F = Vector2F(x * other.width, y * other.height)
+operator fun Vector2F.div(other: Size): Vector2F = Vector2F(x / other.width, y / other.height)
+operator fun Vector2F.rem(other: Size): Vector2F = Vector2F(x % other.width, y % other.height)
+
+fun Point.toSize(): Size = Size(x, y)
+
+fun Size.toInt(): SizeInt = SizeInt(width.toInt(), height.toInt())
+fun Size.toPoint(): Point = Point(width, height)
+fun Size.toVector(): Vector2D = Vector2D(width, height)
+fun Size.toVector2D(): Vector2D = Vector2D(width, height)
+fun Size.toVector2F(): Vector2F = Vector2F(width, height)
+
+interface Sizeable {
+ val size: Size
+
+ companion object {
+ operator fun invoke(size: Size): Sizeable = object : Sizeable {
+ override val size: Size get() = size
+ }
+ }
+}
+
+interface SizeableInt {
+ val size: SizeInt
+ companion object {
+ operator fun invoke(size: SizeInt): SizeableInt = object : SizeableInt {
+ override val size: SizeInt get() = size
+ }
+ operator fun invoke(width: Int, height: Int): SizeableInt = invoke(SizeInt(width, height))
+ }
+}
+
+typealias SizeI = SizeInt
+
+data class SizeInt(val width: Int, val height: Int) {
+ constructor() : this(0, 0)
+
+ fun avgComponent(): Int = (width + height) / 2
+ fun minComponent(): Int = kotlin.math.min(width, height)
+ fun maxComponent(): Int = kotlin.math.max(width, height)
+
+ val area: Int get() = width * height
+ val perimeter: Int get() = width * 2 + height * 2
+
+ operator fun unaryMinus(): SizeInt = SizeInt(-width, -height)
+ operator fun unaryPlus(): SizeInt = this
+
+ operator fun minus(other: SizeInt): SizeInt = SizeInt(width - other.width, height - other.height)
+ operator fun plus(other: SizeInt): SizeInt = SizeInt(width + other.width, height + other.height)
+ operator fun times(s: Float): SizeInt = SizeInt((width * s).toInt(), (height * s).toInt())
+ operator fun times(s: Double): SizeInt = times(s.toFloat())
+ operator fun times(s: Int): SizeInt = times(s.toFloat())
+ operator fun times(scale: Vector2F): SizeInt = SizeInt((width * scale.x).toInt(), (height * scale.y).toInt())
+ operator fun times(scale: Scale): SizeInt = SizeInt((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt())
+
+ operator fun div(other: SizeInt): SizeInt = SizeInt(width / other.width, height / other.height)
+ operator fun div(s: Float): SizeInt = SizeInt((width / s).toInt(), (height / s).toInt())
+ operator fun div(s: Double): SizeInt = div(s.toFloat())
+ operator fun div(s: Int): SizeInt = div(s.toFloat())
+
+ override fun toString(): String = "${width}x${height}"
+}
+
+fun Vector2I.toSize(): SizeInt = SizeInt(x, y)
+fun SizeInt.toFloat(): Size = Size(width.toFloat(), height.toFloat())
+fun SizeInt.toDouble(): Size = Size(width.toDouble(), height.toDouble())
+fun SizeInt.toVector(): Vector2I = Vector2I(width, height)
+
+operator fun Vector2D.plus(other: SizeInt): Vector2D = Vector2D(x + other.width, y + other.height)
+operator fun Vector2D.minus(other: SizeInt): Vector2D = Vector2D(x - other.width, y - other.height)
+operator fun Vector2D.times(other: SizeInt): Vector2D = Vector2D(x * other.width, y * other.height)
+operator fun Vector2D.div(other: SizeInt): Vector2D = Vector2D(x / other.width, y / other.height)
+operator fun Vector2D.rem(other: SizeInt): Vector2D = Vector2D(x % other.width, y % other.height)
+
+operator fun Vector2F.plus(other: SizeInt): Vector2F = Vector2F(x + other.width, y + other.height)
+operator fun Vector2F.minus(other: SizeInt): Vector2F = Vector2F(x - other.width, y - other.height)
+operator fun Vector2F.times(other: SizeInt): Vector2F = Vector2F(x * other.width, y * other.height)
+operator fun Vector2F.div(other: SizeInt): Vector2F = Vector2F(x / other.width, y / other.height)
+operator fun Vector2F.rem(other: SizeInt): Vector2F = Vector2F(x % other.width, y % other.height)
+
+operator fun Vector2I.plus(other: SizeInt): Vector2I = Vector2I(x + other.width, y + other.height)
+operator fun Vector2I.minus(other: SizeInt): Vector2I = Vector2I(x - other.width, y - other.height)
+operator fun Vector2I.times(other: SizeInt): Vector2I = Vector2I(x * other.width, y * other.height)
+operator fun Vector2I.div(other: SizeInt): Vector2I = Vector2I(x / other.width, y / other.height)
+operator fun Vector2I.rem(other: SizeInt): Vector2I = Vector2I(x % other.width, y % other.height)
diff --git a/math/src/main/java/com/icegps/math/geometry/Spacing.kt b/math/src/main/java/com/icegps/math/geometry/Spacing.kt
new file mode 100644
index 00000000..a21164ec
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Spacing.kt
@@ -0,0 +1,28 @@
+package com.icegps.math.geometry
+
+import com.icegps.number.*
+
+data class Spacing(
+ val vertical: Double,
+ val horizontal: Double
+) {
+ operator fun unaryMinus(): Spacing = Spacing(-vertical, -horizontal)
+ operator fun unaryPlus(): Spacing = this
+ operator fun plus(other: Spacing): Spacing = Spacing(vertical + other.vertical, horizontal + other.horizontal)
+ operator fun minus(other: Spacing): Spacing = Spacing(vertical - other.vertical, horizontal - other.horizontal)
+ operator fun times(scale: Double): Spacing = Spacing(vertical * scale, horizontal * scale)
+ operator fun div(scale: Double): Spacing = this * (1.0 / scale)
+ operator fun rem(scale: Double): Spacing = Spacing(vertical % scale, horizontal % scale)
+ operator fun rem(scale: Spacing): Spacing = Spacing(vertical % scale.vertical, horizontal % scale.horizontal)
+
+ companion object {
+ val ZERO = Spacing(0.0, 0.0)
+
+ inline operator fun invoke(spacing: Number): Spacing = Spacing(spacing.toDouble(), spacing.toDouble())
+ inline operator fun invoke(vertical: Number, horizontal: Number): Spacing = Spacing(vertical.toDouble(), horizontal.toDouble())
+ }
+
+ constructor(spacing: Double) : this(spacing, spacing)
+
+ override fun toString(): String = "Spacing(vertical=${vertical.niceStr}, horizontal=${horizontal.niceStr})"
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt b/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt
new file mode 100644
index 00000000..fa7646b3
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt
@@ -0,0 +1,13 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.math.geometry.shape.*
+
+//inline class Sphere3D private constructor(private val data: Float4) : Shape3D {
+data class Sphere3D(override val center: Vector3F, val radius: Float) : SimpleShape3D {
+ //constructor(center: Vector3, radius: Float) : this(Float4(center.x, center.y, center.z, radius))
+ //override val center: Vector3 get() = Vector3(data.x, data.y, data.z)
+ //val radius: Float get() = data.w
+
+ override val volume: Float get() = ((4f / 3f) * PIF) * (radius * radius * radius)
+}
diff --git a/math/src/main/java/com/icegps/math/geometry/VectorExt.kt b/math/src/main/java/com/icegps/math/geometry/VectorExt.kt
new file mode 100644
index 00000000..a9177074
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/VectorExt.kt
@@ -0,0 +1,47 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.interpolation.*
+
+inline fun Vector2F.deltaTransformed(m: Matrix): Vector2F = m.deltaTransform(this)
+inline fun Vector2F.transformed(m: Matrix): Vector2F = m.transform(this)
+fun Vector2F.transformX(m: Matrix): Float = m.transform(this).x
+fun Vector2F.transformY(m: Matrix): Float = m.transform(this).y
+inline fun Vector2F.transformedNullable(m: Matrix?): Vector2F = if (m != null && m.isNotNIL) m.transform(this) else this
+fun Vector2F.transformNullableX(m: Matrix?): Float = if (m != null && m.isNotNIL) m.transform(this).x else x
+fun Vector2F.transformNullableY(m: Matrix?): Float = if (m != null && m.isNotNIL) m.transform(this).y else y
+
+inline fun Vector2D.deltaTransformed(m: Matrix): Vector2D = m.deltaTransform(this)
+inline fun Vector2D.transformed(m: Matrix): Vector2D = m.transform(this)
+fun Vector2D.transformX(m: Matrix): Double = m.transform(this).x
+fun Vector2D.transformY(m: Matrix): Double = m.transform(this).y
+inline fun Vector2D.transformedNullable(m: Matrix?): Vector2D = if (m != null && m.isNotNIL) m.transform(this) else this
+fun Vector2D.transformNullableX(m: Matrix?): Double = if (m != null && m.isNotNIL) m.transform(this).x else x
+fun Vector2D.transformNullableY(m: Matrix?): Double = if (m != null && m.isNotNIL) m.transform(this).y else y
+
+fun List.bounds(): Rectangle = BoundsBuilder(size) { this + get(it) }.bounds
+fun Iterable.bounds(): Rectangle {
+ var bb = BoundsBuilder()
+ for (p in this) bb += p
+ return bb.bounds
+}
+
+
+//inline operator fun Vector2F.plus(that: Size): Vector2F = Vector2F(x + that.width, y + that.height)
+//inline operator fun Vector2F.minus(that: Size): Vector2F = Vector2F(x - that.width, y - that.height)
+//inline operator fun Vector2F.times(that: Size): Vector2F = Vector2F(x * that.width, y * that.height)
+//inline operator fun Vector2F.times(that: Scale): Vector2F = Vector2F(x * that.scaleX, y * that.scaleY)
+//inline operator fun Vector2F.div(that: Size): Vector2F = Vector2F(x / that.width, y / that.height)
+//inline operator fun Vector2F.rem(that: Size): Vector2F = Vector2F(x % that.width, y % that.height)
+
+@Deprecated("", ReplaceWith("ratio.interpolate(this, other)", "com.icegps.math.interpolation.interpolate"))
+fun Vector2F.interpolateWith(ratio: Ratio, other: Vector2F): Vector2F = ratio.interpolate(this, other)
+
+// inline operator fun Vector2D.plus(that: Size): Vector2D = Vector2D(x + that.width, y + that.height)
+// inline operator fun Vector2D.minus(that: Size): Vector2D = Vector2D(x - that.width, y - that.height)
+// inline operator fun Vector2D.times(that: Size): Vector2D = Vector2D(x * that.width, y * that.height)
+// inline operator fun Vector2D.times(that: Scale): Vector2D = Vector2D(x * that.scaleX, y * that.scaleY)
+// inline operator fun Vector2D.div(that: Size): Vector2D = Vector2D(x / that.width, y / that.height)
+// inline operator fun Vector2D.rem(that: Size): Vector2D = Vector2D(x % that.width, y % that.height)
+
+@Deprecated("", ReplaceWith("ratio.interpolate(this, other)", "com.icegps.math.interpolation.interpolate"))
+fun Vector2D.interpolateWith(ratio: Ratio, other: Vector2D): Vector2D = ratio.interpolate(this, other)
diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt b/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt
new file mode 100644
index 00000000..c8981634
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt
@@ -0,0 +1,343 @@
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.number.*
+import kotlin.math.*
+
+typealias Point = Vector2D
+typealias Point2 = Vector2D
+typealias Point3 = Vector3D
+
+data class Vector3D(val x: Double, val y: Double, val z: Double) {
+
+ constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble())
+ constructor(x: Int, y: Int, z: Int) : this(x.toDouble(), y.toDouble(), z.toDouble())
+
+ constructor() : this(0.0, 0.0, 0.0)
+
+ inline operator fun unaryMinus(): Vector3D = Vector3D(-x, -y, -z)
+ inline operator fun unaryPlus(): Vector3D = this
+
+ inline operator fun plus(that: Vector3D): Vector3D = Vector3D(x + that.x, y + that.y, z + that.z)
+ inline operator fun minus(that: Vector3D): Vector3D = Vector3D(x - that.x, y - that.y, z - that.z)
+ inline operator fun times(that: Vector3D): Vector3D = Vector3D(x * that.x, y * that.y, z * that.z)
+ inline operator fun div(that: Vector3D): Vector3D = Vector3D(x / that.x, y / that.y, z / that.z)
+ inline operator fun rem(that: Vector3D): Vector3D = Vector3D(x % that.x, y % that.y, z % that.z)
+
+ inline operator fun times(scale: Double): Vector3D = Vector3D(x * scale, y * scale, z * scale)
+ inline operator fun times(scale: Float): Vector3D = this * scale.toDouble()
+ inline operator fun times(scale: Int): Vector3D = this * scale.toDouble()
+
+ inline operator fun div(scale: Double): Vector3D = Vector3D(x / scale, y / scale, z / scale)
+ inline operator fun div(scale: Float): Vector3D = this / scale.toDouble()
+ inline operator fun div(scale: Int): Vector3D = this / scale.toDouble()
+
+ inline operator fun rem(scale: Double): Vector3D = Vector3D(x % scale, y % scale, z % scale)
+ inline operator fun rem(scale: Float): Vector3D = this % scale.toDouble()
+ inline operator fun rem(scale: Int): Vector3D = this % scale.toDouble()
+
+ fun distanceTo(x: Double, y: Double, z: Double): Double = hypot(hypot(x - this.x, y - this.y), z - this.z)
+ fun distanceTo(x: Float, y: Float, z: Float): Double = distanceTo(x.toDouble(), y.toDouble(), z.toDouble())
+ fun distanceTo(x: Int, y: Int, z: Int): Double = this.distanceTo(x.toDouble(), y.toDouble(), z.toDouble())
+ fun distanceTo(that: Vector3D): Double = distanceTo(that.x, that.y, that.z)
+
+ val length: Double get() = sqrt(x * x + y * y + z * z)
+
+ fun normalized(): Vector3D {
+ val len = length
+ return if (len == 0.0) Vector3D(0.0, 0.0, 0.0) else this * (1.0 / len)
+ }
+
+ infix fun cross(that: Vector3D) = Vector3D(
+ y * that.z - z * that.y,
+ z * that.x - x * that.z,
+ x * that.y - y * that.x
+ )
+
+ infix fun dot(that: Vector3D): Double = x * that.x + y * that.y + z * that.z
+
+ companion object {
+ val FORWARD: Vector3D = Vector3D(0.0, 1.0, 0.0) // +Y 指向北
+ val BACK: Vector3D = Vector3D(0.0, -1.0, 0.0) // -Y 指向南
+ val RIGHT: Vector3D = Vector3D(1.0, 0.0, 0.0) // +X 指向东
+ val LEFT: Vector3D = Vector3D(-1.0, 0.0, 0.0) // -X 指向西
+ val UP: Vector3D = Vector3D(0.0, 0.0, 1.0) // +Z 指向天
+ val DOWN: Vector3D = Vector3D(0.0, 0.0, -1.0) // -Z 指向地
+ }
+}
+
+fun Vector3D.toVector2D(): Vector2D = Vector2D(x, y)
+
+data class Vector4D(val x: Double, val y: Double, val z: Double, val w: Double)
+
+fun Vector3F.toDouble(): Vector3D = Vector3D(x.toDouble(), y.toDouble(), z.toDouble())
+fun Vector3D.toFloat(): Vector3F = Vector3F(x, y, z)
+
+data class Vector2D(val x: Double, val y: Double) : IsAlmostEquals {
+ //constructor(x: Float, y: Float) : this(float2PackOf(x, y))
+ constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble())
+ constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())
+
+ constructor(x: Double, y: Int) : this(x.toDouble(), y.toDouble())
+ constructor(x: Int, y: Double) : this(x.toDouble(), y.toDouble())
+
+ constructor(x: Float, y: Int) : this(x.toDouble(), y.toDouble())
+ constructor(x: Int, y: Float) : this(x.toDouble(), y.toDouble())
+
+ //constructor(p: Vector2) : this(p.raw)
+ constructor() : this(0.0, 0.0)
+ //constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())
+ //constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble())
+
+ fun copy(x: Float = this.x.toFloat(), y: Float = this.y.toFloat()): Vector2D = Vector2D(x, y)
+
+ inline operator fun unaryMinus(): Vector2D = Vector2D(-x, -y)
+ inline operator fun unaryPlus(): Vector2D = this
+
+ inline operator fun plus(that: Vector2D): Vector2D = Vector2D(x + that.x, y + that.y)
+ inline operator fun minus(that: Vector2D): Vector2D = Vector2D(x - that.x, y - that.y)
+ inline operator fun times(that: Vector2D): Vector2D = Vector2D(x * that.x, y * that.y)
+ inline operator fun div(that: Vector2D): Vector2D = Vector2D(x / that.x, y / that.y)
+ inline operator fun rem(that: Vector2D): Vector2D = Vector2D(x % that.x, y % that.y)
+
+ inline operator fun times(scale: Double): Vector2D = Vector2D(x * scale, y * scale)
+ inline operator fun times(scale: Float): Vector2D = this * scale.toDouble()
+ inline operator fun times(scale: Int): Vector2D = this * scale.toDouble()
+
+ inline operator fun div(scale: Double): Vector2D = Vector2D(x / scale, y / scale)
+ inline operator fun div(scale: Float): Vector2D = this / scale.toDouble()
+ inline operator fun div(scale: Int): Vector2D = this / scale.toDouble()
+
+ inline operator fun rem(scale: Double): Vector2D = Vector2D(x % scale, y % scale)
+ inline operator fun rem(scale: Float): Vector2D = this % scale.toDouble()
+ inline operator fun rem(scale: Int): Vector2D = this % scale.toDouble()
+
+ fun avgComponent(): Double = x * 0.5 + y * 0.5
+ fun minComponent(): Double = min(x, y)
+ fun maxComponent(): Double = max(x, y)
+
+ fun distanceTo(x: Double, y: Double): Double = hypot(x - this.x, y - this.y)
+ fun distanceTo(x: Float, y: Float): Double = distanceTo(x.toDouble(), y.toDouble())
+ fun distanceTo(x: Int, y: Int): Double = this.distanceTo(x.toDouble(), y.toDouble())
+ fun distanceTo(that: Vector2D): Double = distanceTo(that.x, that.y)
+
+ infix fun cross(that: Vector2D): Double = crossProduct(this, that)
+ infix fun dot(that: Vector2D): Double = ((this.x * that.x) + (this.y * that.y))
+
+ fun angleTo(other: Vector2D, up: Vector2D = UP): Angle = Angle.between(this.x, this.y, other.x, other.y, up)
+ val angle: Angle get() = angle()
+ fun angle(up: Vector2D = UP): Angle = Angle.between(0.0, 0.0, this.x, this.y, up)
+
+ operator fun get(component: Int): Double = when (component) {
+ 0 -> x; 1 -> y
+ else -> throw IndexOutOfBoundsException("Point doesn't have $component component")
+ }
+ val length: Double get() = hypot(x, y)
+ val lengthSquared: Double get() {
+ val x = x
+ val y = y
+ return x*x + y*y
+ }
+ val magnitude: Double get() = hypot(x, y)
+ val normalized: Vector2D get() = this * (1f / magnitude)
+ val unit: Vector2D get() = this / length
+
+ /** Normal vector. Rotates the vector/point -90 degrees (not normalizing it) */
+ fun toNormal(): Vector2D = Vector2D(-this.y, this.x)
+
+
+ val int: Vector2I get() = Vector2I(x.toInt(), y.toInt())
+ val intRound: Vector2I get() = Vector2I(x.roundToInt(), y.roundToInt())
+
+ fun roundDecimalPlaces(places: Int): Vector2D = Vector2D(x.roundDecimalPlaces(places), y.roundDecimalPlaces(places))
+ fun round(): Vector2D = Vector2D(round(x), round(y))
+ fun ceil(): Vector2D = Vector2D(ceil(x), ceil(y))
+ fun floor(): Vector2D = Vector2D(floor(x), floor(y))
+
+ //fun copy(x: Double = this.x, y: Double = this.y): Vector2 = Vector2D(x, y)
+
+ override fun isAlmostEquals(other: Vector2D, epsilon: Double): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon)
+
+ val niceStr: String get() = "(${x.niceStr}, ${y.niceStr})"
+ fun niceStr(decimalPlaces: Int): String = "(${x.niceStr(decimalPlaces)}, ${y.niceStr(decimalPlaces)})"
+ override fun toString(): String = niceStr
+
+ fun reflected(normal: Vector2D): Vector2D {
+ val d = this
+ val n = normal
+ return d - 2.0 * (d dot n) * n
+ }
+
+ /** Vector2 with inverted (1f / v) components to this */
+ fun inv(): Vector2D = Vector2D(1.0 / x, 1.0 / y)
+
+ fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN()
+
+ val absoluteValue: Vector2D get() = Vector2D(abs(x), abs(y))
+
+ companion object {
+ val ZERO = Vector2D(0.0, 0.0)
+ val NaN = Vector2D(Double.NaN, Double.NaN)
+
+ /** Mathematically typical LEFT, matching screen coordinates (-1, 0) */
+ val LEFT = Vector2D(-1.0, 0.0)
+ /** Mathematically typical RIGHT, matching screen coordinates (+1, 0) */
+ val RIGHT = Vector2D(+1.0, 0.0)
+
+ /** Mathematically typical UP (0, +1) */
+ val UP = Vector2D(0.0, +1.0)
+ /** UP using screen coordinates as reference (0, -1) */
+ val UP_SCREEN = Vector2D(0.0, -1.0)
+
+ /** Mathematically typical DOWN (0, -1) */
+ val DOWN = Vector2D(0.0, -1.0)
+ /** DOWN using screen coordinates as reference (0, +1) */
+ val DOWN_SCREEN = Vector2D(0.0, +1.0)
+
+
+ inline operator fun invoke(x: Number, y: Number): Vector2D = Vector2D(x.toDouble(), y.toDouble())
+ //inline operator fun invoke(x: Float, y: Float): Vector2D = Vector2D(x.toDouble(), y.toDouble())
+
+ //fun fromRaw(raw: Float2Pack) = Vector2D(raw)
+
+ /** Constructs a point from polar coordinates determined by an [angle] and a [length]. Angle 0 is pointing to the right, and the direction is counter-clock-wise for up=UP and clock-wise for up=UP_SCREEN */
+ inline fun polar(x: Float, y: Float, angle: Angle, length: Float = 1f, up: Vector2D = UP): Vector2D = Vector2D(x + angle.cosine(up) * length, y + angle.sine(up) * length)
+ inline fun polar(x: Double, y: Double, angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = Vector2D(x + angle.cosine(up) * length, y + angle.sine(up) * length)
+ inline fun polar(base: Vector2D, angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = polar(base.x, base.y, angle, length, up)
+ inline fun polar(angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = polar(0.0, 0.0, angle, length, up)
+
+ inline fun middle(a: Vector2D, b: Vector2D): Vector2D = (a + b) * 0.5
+
+ fun angle(ax: Double, ay: Double, bx: Double, by: Double, up: Vector2D = UP): Angle = Angle.between(ax, ay, bx, by, up)
+ fun angle(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double, up: Vector2D = UP): Angle = Angle.between(x1 - x2, y1 - y2, x1 - x3, y1 - y3, up)
+
+ fun angle(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.between(a, b, up)
+ fun angle(p1: Vector2D, p2: Vector2D, p3: Vector2D, up: Vector2D = UP): Angle = Angle.between(p1 - p2, p1 - p3, up)
+
+ fun angleArc(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.fromRadians(acos((a dot b) / (a.length * b.length))).adjustFromUp(up)
+ fun angleFull(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.between(a, b, up)
+
+ fun distance(a: Double, b: Double): Double = abs(a - b)
+ fun distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = hypot(x1 - x2, y1 - y2)
+ fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Double = hypot(x1 - x2, y1 - y2).toDouble()
+ fun distance(x1: Int, y1: Int, x2: Int, y2: Int): Double = hypot(x1.toDouble() - x2.toDouble(), y1.toDouble() - y2.toDouble())
+ fun distance(a: Vector2D, b: Vector2D): Double = distance(a.x, a.y, b.x, b.y)
+ fun distance(a: Vector2I, b: Vector2I): Double = distance(a.x, a.y, b.x, b.y)
+
+ fun distanceSquared(a: Vector2D, b: Vector2D): Double = distanceSquared(a.x, a.y, b.x, b.y)
+ fun distanceSquared(a: Vector2I, b: Vector2I): Int = distanceSquared(a.x, a.y, b.x, b.y)
+ fun distanceSquared(x1: Double, y1: Double, x2: Double, y2: Double): Double = square(x1 - x2) + square(y1 - y2)
+ fun distanceSquared(x1: Float, y1: Float, x2: Float, y2: Float): Float = square(x1 - x2) + square(y1 - y2)
+ fun distanceSquared(x1: Int, y1: Int, x2: Int, y2: Int): Int = square(x1 - x2) + square(y1 - y2)
+
+ @Deprecated("Likely searching for orientation")
+ inline fun direction(a: Vector2D, b: Vector2D): Vector2D = b - a
+
+ fun compare(l: Vector2D, r: Vector2D): Int = compare(l.x, l.y, r.x, r.y)
+ fun compare(lx: Float, ly: Float, rx: Float, ry: Float): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret }
+ fun compare(lx: Double, ly: Double, rx: Double, ry: Double): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret }
+
+ private fun square(x: Double): Double = x * x
+ private fun square(x: Float): Float = x * x
+ private fun square(x: Int): Int = x * x
+
+ fun dot(aX: Double, aY: Double, bX: Double, bY: Double): Double = (aX * bX) + (aY * bY)
+ fun dot(aX: Float, aY: Float, bX: Float, bY: Float): Float = (aX * bX) + (aY * bY)
+ fun dot(a: Vector2D, b: Vector2D): Double = dot(a.x, a.y, b.x, b.y)
+
+ fun isCollinear(p1: Point, p2: Point, p3: Point): Boolean =
+ isCollinear(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
+
+ fun isCollinear(p1x: Double, p1y: Double, p2x: Double, p2y: Double, p3x: Double, p3y: Double): Boolean {
+ val area2 = (p1x * (p2y - p3y) + p2x * (p3y - p1y) + p3x * (p1y - p2y)) // 2x triangle area
+ //println("($p1x, $p1y), ($p2x, $p2y), ($p3x, $p3y) :: area=$area2")
+ return area2.isAlmostZero()
+
+ //val div1 = (p2x - p1x) / (p2y - p1y)
+ //val div2 = (p1x - p3x) / (p1y - p3y)
+ //val result = (div1 - div2).absoluteValue
+ //println("result=$result, div1=$div1, div2=$div2, xa=$p1x, ya=$p1y, x=$p2x, y=$p2y, xb=$p3x, yb=$p3y")
+ //if (div1.isInfinite() != div2.isInfinite()) return false
+ //return result.isAlmostZero() || result.isInfinite()
+ }
+
+ fun isCollinear(xa: Float, ya: Float, x: Float, y: Float, xb: Float, yb: Float): Boolean = isCollinear(
+ xa.toDouble(), ya.toDouble(),
+ x.toDouble(), y.toDouble(),
+ xb.toDouble(), yb.toDouble(),
+ )
+
+ fun isCollinear(xa: Int, ya: Int, x: Int, y: Int, xb: Int, yb: Int): Boolean = isCollinear(
+ xa.toDouble(), ya.toDouble(),
+ x.toDouble(), y.toDouble(),
+ xb.toDouble(), yb.toDouble(),
+ )
+
+ // https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
+ /** < 0 left, > 0 right, 0 collinear */
+ fun orientation(p1: Vector2D, p2: Vector2D, p3: Vector2D, up: Vector2D = UP): Double = orientation(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, up)
+ fun orientation(ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float, up: Vector2D = UP): Float {
+ Orientation.checkValidUpVector(up)
+ val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay)
+ return if (up.y > 0f) res else -res
+ }
+ fun orientation(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double, up: Vector2D = UP): Double {
+ Orientation.checkValidUpVector(up)
+ val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay)
+ return if (up.y > 0f) res else -res
+ }
+
+ fun crossProduct(ax: Float, ay: Float, bx: Float, by: Float): Float = (ax * by) - (bx * ay)
+ fun crossProduct(ax: Double, ay: Double, bx: Double, by: Double): Double = (ax * by) - (bx * ay)
+ fun crossProduct(p1: Vector2D, p2: Vector2D): Double = crossProduct(p1.x, p1.y, p2.x, p2.y)
+
+ fun minComponents(p1: Vector2D, p2: Vector2D): Vector2D = Vector2D(min(p1.x, p2.x), min(p1.y, p2.y))
+ fun minComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D): Vector2D = Vector2D(
+ minOf(p1.x, p2.x, p3.x),
+ minOf(p1.y, p2.y, p3.y)
+ )
+ fun minComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D, p4: Vector2D): Vector2D = Vector2D(
+ minOf(
+ p1.x,
+ p2.x,
+ p3.x,
+ p4.x
+ ), minOf(p1.y, p2.y, p3.y, p4.y)
+ )
+ fun maxComponents(p1: Vector2D, p2: Vector2D): Vector2D = Vector2D(max(p1.x, p2.x), max(p1.y, p2.y))
+ fun maxComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D): Vector2D = Vector2D(
+ maxOf(p1.x, p2.x, p3.x),
+ maxOf(p1.y, p2.y, p3.y)
+ )
+ fun maxComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D, p4: Vector2D): Vector2D = Vector2D(
+ maxOf(
+ p1.x,
+ p2.x,
+ p3.x,
+ p4.x
+ ), maxOf(p1.y, p2.y, p3.y, p4.y)
+ )
+ }
+}
+
+operator fun Int.times(v: Vector2D): Vector2D = v * this
+operator fun Float.times(v: Vector2D): Vector2D = v * this
+operator fun Double.times(v: Vector2D): Vector2D = v * this
+
+fun Vector2D.toFloat(): Vector2F = Vector2F(x, y)
+fun Vector2F.toDouble(): Vector2D = Vector2D(x, y)
+
+fun abs(a: Vector2D): Vector2D = a.absoluteValue
+fun min(a: Vector2D, b: Vector2D): Vector2D = Vector2D(min(a.x, b.x), min(a.y, b.y))
+fun max(a: Vector2D, b: Vector2D): Vector2D = Vector2D(max(a.x, b.x), max(a.y, b.y))
+fun Vector2D.clamp(min: Float, max: Float): Vector2D = clamp(min.toDouble(), max.toDouble())
+fun Vector2D.clamp(min: Double, max: Double): Vector2D = Vector2D(x.clamp(min, max), y.clamp(min, max))
+fun Vector2D.clamp(min: Vector2D, max: Vector2D): Vector2D = Vector2D(x.clamp(min.x, max.x), y.clamp(min.y, max.y))
+
+fun Vector2D.toInt(): Vector2I = Vector2I(x.toInt(), y.toInt())
+fun Vector2D.toIntCeil(): Vector2I = Vector2I(x.toIntCeil(), y.toIntCeil())
+fun Vector2D.toIntRound(): Vector2I = Vector2I(x.toIntRound(), y.toIntRound())
+fun Vector2D.toIntFloor(): Vector2I = Vector2I(x.toIntFloor(), y.toIntFloor())
+
+fun Vector3D.toCylindrical(): CylindricalVector = CylindricalVector.fromCartesian(this)
diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt b/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt
new file mode 100644
index 00000000..f6a726ae
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt
@@ -0,0 +1,523 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.icegps.math.geometry
+
+import com.icegps.math.*
+import com.icegps.number.*
+import kotlin.math.*
+
+typealias Vector2 = Vector2F
+typealias Vector3 = Vector3F
+typealias Vector4 = Vector4F
+
+fun vec(x: Float, y: Float): Vector2F = Vector2F(x, y)
+fun vec2(x: Float, y: Float): Vector2F = Vector2F(x, y)
+fun vec(x: Float, y: Float, z: Float): Vector3F = Vector3F(x, y, z)
+fun vec3(x: Float, y: Float, z: Float): Vector3F = Vector3F(x, y, z)
+fun vec(x: Float, y: Float, z: Float, w: Float): Vector4F = Vector4F(x, y, z, w)
+fun vec4(x: Float, y: Float, z: Float, w: Float = 1f): Vector4F = Vector4F(x, y, z, w)
+
+//////////////////////////////
+// VALUE CLASSES
+//////////////////////////////
+
+//@Deprecated("", ReplaceWith("p", "com.icegps.math.geometry.Point")) fun Point(p: Vector2F): Vector2F = p
+//@Deprecated("", ReplaceWith("p", "com.icegps.math.geometry.Vector2")) fun Vector2(p: Vector2F): Vector2F = p
+
+data class Vector2F(val x: Float, val y: Float) {
+ constructor(x: Double, y: Double) : this(x.toFloat(), y.toFloat())
+ constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())
+
+ constructor(x: Double, y: Int) : this(x.toFloat(), y.toFloat())
+ constructor(x: Int, y: Double) : this(x.toFloat(), y.toFloat())
+
+ constructor(x: Float, y: Int) : this(x.toFloat(), y.toFloat())
+ constructor(x: Int, y: Float) : this(x.toFloat(), y.toFloat())
+
+ //constructor(p: Vector2) : this(p.raw)
+ constructor() : this(0f, 0f)
+ //constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble())
+ //constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble())
+
+ fun copy(x: Double = this.x.toDouble(), y: Double = this.y.toDouble()): Vector2F = Vector2F(x, y)
+
+ inline operator fun unaryMinus(): Vector2F = Vector2F(-x, -y)
+ inline operator fun unaryPlus(): Vector2F = this
+
+ inline operator fun plus(that: Vector2F): Vector2F = Vector2F(x + that.x, y + that.y)
+ inline operator fun minus(that: Vector2F): Vector2F = Vector2F(x - that.x, y - that.y)
+ inline operator fun times(that: Vector2F): Vector2F = Vector2F(x * that.x, y * that.y)
+ inline operator fun div(that: Vector2F): Vector2F = Vector2F(x / that.x, y / that.y)
+ inline operator fun rem(that: Vector2F): Vector2F = Vector2F(x % that.x, y % that.y)
+
+ inline operator fun times(scale: Float): Vector2F = Vector2F(x * scale, y * scale)
+ inline operator fun times(scale: Double): Vector2F = this * scale.toFloat()
+ inline operator fun times(scale: Int): Vector2F = this * scale.toDouble()
+
+ inline operator fun div(scale: Float): Vector2F = Vector2F(x / scale, y / scale)
+ inline operator fun div(scale: Double): Vector2F = this / scale.toFloat()
+ inline operator fun div(scale: Int): Vector2F = this / scale.toDouble()
+
+ inline operator fun rem(scale: Float): Vector2F = Vector2F(x % scale, y % scale)
+ inline operator fun rem(scale: Double): Vector2F = this % scale.toFloat()
+ inline operator fun rem(scale: Int): Vector2F = this % scale.toDouble()
+
+ fun avgComponent(): Float = x * 0.5f + y * 0.5f
+ fun minComponent(): Float = min(x, y)
+ fun maxComponent(): Float = max(x, y)
+
+ fun distanceTo(x: Float, y: Float): Float = hypot(x - this.x, y - this.y)
+ fun distanceTo(x: Double, y: Double): Float = this.distanceTo(x.toFloat(), y.toFloat())
+ fun distanceTo(x: Int, y: Int): Float = this.distanceTo(x.toDouble(), y.toDouble())
+ fun distanceTo(that: Vector2F): Float = distanceTo(that.x, that.y)
+
+ infix fun cross(that: Vector2F): Float = crossProduct(this, that)
+ infix fun dot(that: Vector2F): Float = ((this.x * that.x) + (this.y * that.y))
+
+ fun angleTo(other: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(this.x, this.y, other.x, other.y, up)
+ val angle: Angle get() = angle()
+ fun angle(up: Vector2D = Vector2D.UP): Angle = Angle.between(0f, 0f, this.x, this.y, up)
+
+ operator fun get(component: Int) = when (component) {
+ 0 -> x; 1 -> y
+ else -> throw IndexOutOfBoundsException("Point doesn't have $component component")
+ }
+ val length: Float get() = hypot(x, y)
+ val lengthSquared: Float get() {
+ val x = x
+ val y = y
+ return x*x + y*y
+ }
+ val magnitude: Float get() = hypot(x, y)
+ val normalized: Vector2F get() = this * (1f / magnitude)
+ val unit: Vector2F get() = this / length
+
+ /** Normal vector. Rotates the vector/point -90 degrees (not normalizing it) */
+ fun toNormal(): Vector2F = Vector2F(-this.y, this.x)
+
+
+ val int: Vector2I get() = Vector2I(x.toInt(), y.toInt())
+ val intRound: Vector2I get() = Vector2I(x.roundToInt(), y.roundToInt())
+
+ fun roundDecimalPlaces(places: Int): Vector2F = Vector2F(x.roundDecimalPlaces(places), y.roundDecimalPlaces(places))
+ fun round(): Vector2F = Vector2F(round(x), round(y))
+ fun ceil(): Vector2F = Vector2F(ceil(x), ceil(y))
+ fun floor(): Vector2F = Vector2F(floor(x), floor(y))
+
+ //fun copy(x: Double = this.x, y: Double = this.y): Vector2 = Point(x, y)
+
+ fun isAlmostEquals(other: Vector2F, epsilon: Float = 0.00001f): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon)
+
+ val niceStr: String get() = "(${x.niceStr}, ${y.niceStr})"
+ fun niceStr(decimalPlaces: Int): String = "(${x.niceStr(decimalPlaces)}, ${y.niceStr(decimalPlaces)})"
+ override fun toString(): String = niceStr
+
+ fun reflected(normal: Vector2F): Vector2F {
+ val d = this
+ val n = normal
+ return d - 2f * (d dot n) * n
+ }
+
+ /** Vector2 with inverted (1f / v) components to this */
+ fun inv(): Vector2F = Vector2F(1f / x, 1f / y)
+
+ fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN()
+
+ val absoluteValue: Vector2F get() = Vector2F(abs(x), abs(y))
+
+ companion object {
+ val ZERO = Vector2F(0f, 0f)
+ val NaN = Vector2F(Float.NaN, Float.NaN)
+
+ /** Mathematically typical LEFT, matching screen coordinates (-1, 0) */
+ val LEFT = Vector2F(-1f, 0f)
+ /** Mathematically typical RIGHT, matching screen coordinates (+1, 0) */
+ val RIGHT = Vector2F(+1f, 0f)
+
+ /** Mathematically typical UP (0, +1) */
+ val UP = Vector2F(0f, +1f)
+ /** UP using 2D screen coordinates as reference (0, -1) */
+ val UP_SCREEN = Vector2F(0f, -1f)
+
+ /** Mathematically typical DOWN (0, -1) */
+ val DOWN = Vector2F(0f, -1f)
+ /** DOWN using 2D screen coordinates as reference (0, +1) */
+ val DOWN_SCREEN = Vector2F(0f, +1f)
+
+
+ //inline operator fun invoke(x: Int, y: Int): Vector2 = Point(x.toDouble(), y.toDouble())
+ //inline operator fun invoke(x: Float, y: Float): Vector2 = Point(x.toDouble(), y.toDouble())
+
+ //fun fromRaw(raw: Float2Pack) = Point(raw)
+
+ /** Constructs a point from polar coordinates determined by an [angle] and a [length]. Angle 0 is pointing to the right, and the direction is counter-clock-wise for up=UP and clock-wise for up=UP_SCREEN */
+ inline fun polar(x: Float, y: Float, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = Vector2F(x + angle.cosine(up) * length, y + angle.sine(up) * length)
+ inline fun polar(x: Double, y: Double, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = Vector2F(x + angle.cosine(up) * length, y + angle.sine(up) * length)
+ inline fun polar(base: Vector2F, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = polar(base.x, base.y, angle, length, up)
+ inline fun polar(angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = polar(0.0, 0.0, angle, length, up)
+
+ inline fun middle(a: Vector2F, b: Vector2F): Vector2F = (a + b) * 0.5
+
+ fun angle(ax: Double, ay: Double, bx: Double, by: Double, up: Vector2D = Vector2D.UP): Angle = Angle.between(ax, ay, bx, by, up)
+ fun angle(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double, up: Vector2D = Vector2D.UP): Angle = Angle.between(x1 - x2, y1 - y2, x1 - x3, y1 - y3, up)
+
+ fun angle(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(a, b, up)
+ fun angle(p1: Vector2F, p2: Vector2F, p3: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(p1 - p2, p1 - p3, up)
+
+ fun angleArc(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.fromRadians(acos((a dot b) / (a.length * b.length))).adjustFromUp(up)
+ fun angleFull(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(a, b, up)
+
+ fun distance(a: Double, b: Double): Double = abs(a - b)
+ fun distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = hypot(x1 - x2, y1 - y2)
+ fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float = hypot(x1 - x2, y1 - y2)
+ fun distance(x1: Int, y1: Int, x2: Int, y2: Int): Float = distance(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat())
+ fun distance(a: Vector2F, b: Vector2F): Float = distance(a.x, a.y, b.x, b.y)
+ fun distance(a: Vector2I, b: Vector2I): Float = distance(a.x, a.y, b.x, b.y)
+
+ fun distanceSquared(a: Vector2F, b: Vector2F): Float = distanceSquared(a.x, a.y, b.x, b.y)
+ fun distanceSquared(a: Vector2I, b: Vector2I): Int = distanceSquared(a.x, a.y, b.x, b.y)
+ fun distanceSquared(x1: Double, y1: Double, x2: Double, y2: Double): Double = square(x1 - x2) + square(y1 - y2)
+ fun distanceSquared(x1: Float, y1: Float, x2: Float, y2: Float): Float = square(x1 - x2) + square(y1 - y2)
+ fun distanceSquared(x1: Int, y1: Int, x2: Int, y2: Int): Int = square(x1 - x2) + square(y1 - y2)
+
+ @Deprecated("Likely searching for orientation")
+ inline fun direction(a: Vector2F, b: Vector2F): Vector2F = b - a
+
+ fun compare(l: Vector2F, r: Vector2F): Int = compare(l.x, l.y, r.x, r.y)
+ fun compare(lx: Float, ly: Float, rx: Float, ry: Float): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret }
+ fun compare(lx: Double, ly: Double, rx: Double, ry: Double): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret }
+
+ private fun square(x: Double): Double = x * x
+ private fun square(x: Float): Float = x * x
+ private fun square(x: Int): Int = x * x
+
+ fun dot(aX: Double, aY: Double, bX: Double, bY: Double): Double = (aX * bX) + (aY * bY)
+ fun dot(aX: Float, aY: Float, bX: Float, bY: Float): Float = (aX * bX) + (aY * bY)
+ fun dot(a: Vector2F, b: Vector2F): Float = dot(a.x, a.y, b.x, b.y)
+
+ fun isCollinear(p1: Point, p2: Point, p3: Point): Boolean =
+ isCollinear(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
+
+ fun isCollinear(p1x: Float, p1y: Float, p2x: Float, p2y: Float, p3x: Float, p3y: Float): Boolean {
+ val area2 = (p1x * (p2y - p3y) + p2x * (p3y - p1y) + p3x * (p1y - p2y)) // 2x triangle area
+ //println("($p1x, $p1y), ($p2x, $p2y), ($p3x, $p3y) :: area=$area2")
+ return area2.isAlmostZero()
+
+ //val div1 = (p2x - p1x) / (p2y - p1y)
+ //val div2 = (p1x - p3x) / (p1y - p3y)
+ //val result = (div1 - div2).absoluteValue
+ //println("result=$result, div1=$div1, div2=$div2, xa=$p1x, ya=$p1y, x=$p2x, y=$p2y, xb=$p3x, yb=$p3y")
+ //if (div1.isInfinite() != div2.isInfinite()) return false
+ //return result.isAlmostZero() || result.isInfinite()
+ }
+
+ fun isCollinear(xa: Double, ya: Double, x: Double, y: Double, xb: Double, yb: Double): Boolean = isCollinear(
+ xa.toFloat(), ya.toFloat(),
+ x.toFloat(), y.toFloat(),
+ xb.toFloat(), yb.toFloat(),
+ )
+
+ fun isCollinear(xa: Int, ya: Int, x: Int, y: Int, xb: Int, yb: Int): Boolean = isCollinear(
+ xa.toFloat(), ya.toFloat(),
+ x.toFloat(), y.toFloat(),
+ xb.toFloat(), yb.toFloat(),
+ )
+
+ // https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/
+ /** < 0 left, > 0 right, 0 collinear */
+ fun orientation(p1: Vector2F, p2: Vector2F, p3: Vector2F, up: Vector2D = Vector2D.UP): Float = orientation(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, up)
+ fun orientation(ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float, up: Vector2D = Vector2D.UP): Float {
+ Orientation.checkValidUpVector(up)
+ val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay)
+ return if (up.y > 0f) res else -res
+ }
+ fun orientation(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double, up: Vector2D = Vector2D.UP): Double {
+ Orientation.checkValidUpVector(up)
+ val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay)
+ return if (up.y > 0f) res else -res
+ }
+
+ fun crossProduct(ax: Float, ay: Float, bx: Float, by: Float): Float = (ax * by) - (bx * ay)
+ fun crossProduct(ax: Double, ay: Double, bx: Double, by: Double): Double = (ax * by) - (bx * ay)
+ fun crossProduct(p1: Vector2F, p2: Vector2F): Float = crossProduct(p1.x, p1.y, p2.x, p2.y)
+
+ fun minComponents(p1: Vector2F, p2: Vector2F): Vector2F = Vector2F(min(p1.x, p2.x), min(p1.y, p2.y))
+ fun minComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F): Vector2F = Vector2F(
+ minOf(p1.x, p2.x, p3.x),
+ minOf(p1.y, p2.y, p3.y)
+ )
+ fun minComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F, p4: Vector2F): Vector2F = Vector2F(
+ minOf(
+ p1.x,
+ p2.x,
+ p3.x,
+ p4.x
+ ), minOf(p1.y, p2.y, p3.y, p4.y)
+ )
+ fun maxComponents(p1: Vector2F, p2: Vector2F): Vector2F = Vector2F(max(p1.x, p2.x), max(p1.y, p2.y))
+ fun maxComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F): Vector2F = Vector2F(
+ maxOf(p1.x, p2.x, p3.x),
+ maxOf(p1.y, p2.y, p3.y)
+ )
+ fun maxComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F, p4: Vector2F): Vector2F = Vector2F(
+ maxOf(
+ p1.x,
+ p2.x,
+ p3.x,
+ p4.x
+ ), maxOf(p1.y, p2.y, p3.y, p4.y)
+ )
+ }
+}
+
+operator fun Int.times(v: Vector2F): Vector2F = v * this
+operator fun Float.times(v: Vector2F): Vector2F = v * this
+operator fun Double.times(v: Vector2F): Vector2F = v * this
+
+fun abs(a: Vector2F): Vector2F = a.absoluteValue
+fun min(a: Vector2F, b: Vector2F): Vector2F = Vector2F(min(a.x, b.x), min(a.y, b.y))
+fun max(a: Vector2F, b: Vector2F): Vector2F = Vector2F(max(a.x, b.x), max(a.y, b.y))
+fun Vector2F.clamp(min: Float, max: Float): Vector2F = Vector2F(x.clamp(min, max), y.clamp(min, max))
+fun Vector2F.clamp(min: Double, max: Double): Vector2F = clamp(min.toFloat(), max.toFloat())
+fun Vector2F.clamp(min: Vector2F, max: Vector2F): Vector2F = Vector2F(x.clamp(min.x, max.x), y.clamp(min.y, max.y))
+
+fun Vector2F.toInt(): Vector2I = Vector2I(x.toInt(), y.toInt())
+fun Vector2F.toIntCeil(): Vector2I = Vector2I(x.toIntCeil(), y.toIntCeil())
+fun Vector2F.toIntRound(): Vector2I = Vector2I(x.toIntRound(), y.toIntRound())
+fun Vector2F.toIntFloor(): Vector2I = Vector2I(x.toIntFloor(), y.toIntFloor())
+
+
+data class Vector3F(val x: Float, val y: Float, val z: Float) : IsAlmostEqualsF {
+ companion object {
+ val NaN = Vector3F(Float.NaN, Float.NaN, Float.NaN)
+
+ val ZERO = Vector3F(0f, 0f, 0f)
+ val ONE = Vector3F(1f, 1f, 1f)
+
+ val FORWARD = Vector3F(0f, 0f, 1f)
+ val BACK = Vector3F(0f, 0f, -1f)
+ val LEFT = Vector3F(-1f, 0f, 0f)
+ val RIGHT = Vector3F(1f, 0f, 0f)
+ val UP = Vector3F(0f, 1f, 0f)
+ val DOWN = Vector3F(0f, -1f, 0f)
+
+ operator fun invoke(): Vector3F = ZERO
+
+ fun cross(a: Vector3F, b: Vector3F): Vector3F = Vector3F(
+ ((a.y * b.z) - (a.z * b.y)),
+ ((a.z * b.x) - (a.x * b.z)),
+ ((a.x * b.y) - (a.y * b.x)),
+ )
+
+ fun length(x: Float, y: Float, z: Float): Float = sqrt(lengthSq(x, y, z))
+ fun lengthSq(x: Float, y: Float, z: Float): Float = x * x + y * y + z * z
+
+ fun fromArray(array: FloatArray, offset: Int): Vector3F =
+ Vector3F(array[offset + 0], array[offset + 1], array[offset + 2])
+
+ inline fun func(func: (index: Int) -> Float): Vector3F = Vector3F(func(0), func(1), func(2))
+ }
+
+ //constructor(x: Float, y: Float, z: Float) : this(float4PackOf(x, y, z, 0f))
+ constructor(x: Int, y: Int, z: Int) : this(x.toFloat(), y.toFloat(), z.toFloat())
+ constructor(x: Double, y: Double, z: Double) : this(x.toFloat(), y.toFloat(), z.toFloat())
+
+ fun distanceTo(other: Vector3F): Float {
+ val dx = this.x - other.x
+ val dy = this.y - other.y
+ val dz = this.z - other.z
+ return sqrt(dx * dx + dy * dy + dz * dz)
+ }
+
+ val lengthSquared: Float get() = (x * x) + (y * y) + (z * z)
+ val length: Float get() = sqrt(lengthSquared)
+ fun normalized(): Vector3F {
+ val length = this.length
+ //if (length.isAlmostZero()) return Vector3.ZERO
+ if (length == 0f) return Vector3F.ZERO
+ return this / length
+ }
+
+ // https://math.stackexchange.com/questions/13261/how-to-get-a-reflection-vector
+ // 𝑟=𝑑−2(𝑑⋅𝑛)𝑛
+ fun reflected(surfaceNormal: Vector3F): Vector3F {
+ val d = this
+ val n = surfaceNormal
+ return d - 2f * (d dot n) * n
+ }
+
+ operator fun get(index: Int): Float = when (index) {
+ 0 -> x
+ 1 -> y
+ 2 -> z
+ else -> throw IndexOutOfBoundsException()
+ }
+
+ operator fun unaryPlus(): Vector3F = this
+ operator fun unaryMinus(): Vector3F = Vector3F(-this.x, -this.y, -this.z)
+
+ operator fun plus(v: Vector3F): Vector3F = Vector3F(this.x + v.x, this.y + v.y, this.z + v.z)
+ operator fun minus(v: Vector3F): Vector3F = Vector3F(this.x - v.x, this.y - v.y, this.z - v.z)
+
+ operator fun times(v: Vector3F): Vector3F = Vector3F(this.x * v.x, this.y * v.y, this.z * v.z)
+ operator fun div(v: Vector3F): Vector3F = Vector3F(this.x / v.x, this.y / v.y, this.z / v.z)
+ operator fun rem(v: Vector3F): Vector3F = Vector3F(this.x % v.x, this.y % v.y, this.z % v.z)
+
+ operator fun times(v: Float): Vector3F = Vector3F(this.x * v, this.y * v, this.z * v)
+ operator fun div(v: Float): Vector3F = Vector3F(this.x / v, this.y / v, this.z / v)
+ operator fun rem(v: Float): Vector3F = Vector3F(this.x % v, this.y % v, this.z % v)
+
+ operator fun times(v: Int): Vector3F = this * v.toFloat()
+ operator fun div(v: Int): Vector3F = this / v.toFloat()
+ operator fun rem(v: Int): Vector3F = this % v.toFloat()
+
+ operator fun times(v: Double): Vector3F = this * v.toFloat()
+ operator fun div(v: Double): Vector3F = this / v.toFloat()
+ operator fun rem(v: Double): Vector3F = this % v.toFloat()
+
+ infix fun dot(v: Vector3F): Float = (x * v.x) + (y * v.y) + (z * v.z)
+ infix fun cross(v: Vector3F): Vector3F = cross(this, v)
+
+ /** Vector3 with inverted (1f / v) components to this */
+ fun inv(): Vector3F = Vector3F(1f / x, 1f / y, 1f / z)
+
+ fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() && this.z.isNaN()
+ val absoluteValue: Vector3F get() = Vector3F(abs(x), abs(y), abs(z))
+
+ override fun toString(): String = "Vector3(${x.niceStr}, ${y.niceStr}, ${z.niceStr})"
+
+ fun toVector4(w: Float = 1f): Vector4F = Vector4F(x, y, z, w)
+ override fun isAlmostEquals(other: Vector3F, epsilon: Float): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon) &&
+ this.y.isAlmostEquals(other.y, epsilon) &&
+ this.z.isAlmostEquals(other.z, epsilon)
+}
+
+operator fun Int.times(v: Vector3F): Vector3F = v * this
+operator fun Float.times(v: Vector3F): Vector3F = v * this
+operator fun Double.times(v: Vector3F): Vector3F = v * this
+
+fun abs(a: Vector3F): Vector3F = a.absoluteValue
+fun min(a: Vector3F, b: Vector3F): Vector3F = Vector3F(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z))
+fun max(a: Vector3F, b: Vector3F): Vector3F = Vector3F(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z))
+fun Vector3F.clamp(min: Float, max: Float): Vector3F = Vector3F(x.clamp(min, max), y.clamp(min, max), z.clamp(min, max))
+fun Vector3F.clamp(min: Double, max: Double): Vector3F = clamp(min.toFloat(), max.toFloat())
+fun Vector3F.clamp(min: Vector3F, max: Vector3F): Vector3F = Vector3F(x.clamp(min.x, max.x), y.clamp(min.y, max.y), z.clamp(min.z, max.z))
+
+data class Vector4F(val x: Float, val y: Float, val z: Float, val w: Float) {
+ companion object {
+ val ZERO = Vector4F(0f, 0f, 0f, 0f)
+ val ONE = Vector4F(1f, 1f, 1f, 1f)
+
+ operator fun invoke(): Vector4F = Vector4F.ZERO
+
+ fun fromArray(array: FloatArray, offset: Int = 0): Vector4F = Vector4F(array[offset + 0], array[offset + 1], array[offset + 2], array[offset + 3])
+
+ fun length(x: Float, y: Float, z: Float, w: Float): Float = sqrt(lengthSq(x, y, z, w))
+ fun lengthSq(x: Float, y: Float, z: Float, w: Float): Float = x * x + y * y + z * z + w * w
+
+ inline fun func(func: (index: Int) -> Float): Vector4F = Vector4F(func(0), func(1), func(2), func(3))
+ }
+
+ constructor(xyz: Vector3F, w: Float) : this(xyz.x, xyz.y, xyz.z, w)
+ //constructor(x: Float, y: Float, z: Float, w: Float) : this(float4PackOf(x, y, z, w))
+ constructor(x: Int, y: Int, z: Int, w: Int) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+ constructor(x: Double, y: Double, z: Double, w: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat())
+
+ val xyz: Vector3F get() = Vector3F(x, y, z)
+
+ val length3Squared: Float get() = (x * x) + (y * y) + (z * z)
+ /** Only taking into accoount x, y, z */
+ val length3: Float get() = sqrt(length3Squared)
+
+ val lengthSquared: Float get() = (x * x) + (y * y) + (z * z) + (w * w)
+ val length: Float get() = sqrt(lengthSquared)
+
+ fun normalized(): Vector4F {
+ val length = this.length
+ if (length == 0f) return Vector4F.ZERO
+ return this / length
+ }
+
+ operator fun get(index: Int): Float = when (index) {
+ 0 -> x
+ 1 -> y
+ 2 -> z
+ 3 -> w
+ else -> throw IndexOutOfBoundsException()
+ }
+
+ operator fun unaryPlus(): Vector4F = this
+ operator fun unaryMinus(): Vector4F = Vector4F(-x, -y, -z, -w)
+
+ operator fun plus(v: Vector4F): Vector4F = Vector4F(x + v.x, y + v.y, z + v.z, w + v.w)
+ operator fun minus(v: Vector4F): Vector4F = Vector4F(x - v.x, y - v.y, z - v.z, w - v.w)
+
+ operator fun times(v: Vector4F): Vector4F = Vector4F(x * v.x, y * v.y, z * v.z, w * v.w)
+ operator fun div(v: Vector4F): Vector4F = Vector4F(x / v.x, y / v.y, z / v.z, w / v.w)
+ operator fun rem(v: Vector4F): Vector4F = Vector4F(x % v.x, y % v.y, z % v.z, w % v.w)
+
+ operator fun times(v: Float): Vector4F = Vector4F(x * v, y * v, z * v, w * v)
+ operator fun div(v: Float): Vector4F = Vector4F(x / v, y / v, z / v, w / v)
+ operator fun rem(v: Float): Vector4F = Vector4F(x % v, y % v, z % v, w % v)
+
+ infix fun dot(v: Vector4F): Float = (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w)
+ //infix fun cross(v: Vector4): Vector4 = cross(this, v)
+
+ fun copyTo(out: FloatArray, offset: Int = 0): FloatArray {
+ out[offset + 0] = x
+ out[offset + 1] = y
+ out[offset + 2] = z
+ out[offset + 3] = w
+ return out
+ }
+
+ /** Vector4 with inverted (1f / v) components to this */
+ fun inv(): Vector4F = Vector4F(1f / x, 1f / y, 1f / z, 1f / w)
+
+ fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() && this.z.isNaN() && this.w.isNaN()
+ val absoluteValue: Vector4F get() = Vector4F(abs(x), abs(y), abs(z), abs(w))
+
+ override fun toString(): String = "Vector4(${x.niceStr}, ${y.niceStr}, ${z.niceStr}, ${w.niceStr})"
+
+ // @TODO: Should we scale Vector3 by w?
+ fun toVector3(): Vector3F = Vector3F(x, y, z)
+ fun isAlmostEquals(other: Vector4F, epsilon: Float = 0.00001f): Boolean =
+ this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon) && this.z.isAlmostEquals(other.z, epsilon) && this.w.isAlmostEquals(other.w, epsilon)
+}
+
+fun abs(a: Vector4F): Vector4F = a.absoluteValue
+fun min(a: Vector4F, b: Vector4F): Vector4F = Vector4F(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w))
+fun max(a: Vector4F, b: Vector4F): Vector4F = Vector4F(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w))
+fun Vector4F.clamp(min: Float, max: Float): Vector4F = Vector4F(x.clamp(min, max), y.clamp(min, max), z.clamp(min, max), w.clamp(min, max))
+fun Vector4F.clamp(min: Double, max: Double): Vector4F = clamp(min.toFloat(), max.toFloat())
+fun Vector4F.clamp(min: Vector4F, max: Vector4F): Vector4F = Vector4F(x.clamp(min.x, max.x), y.clamp(min.y, max.y), z.clamp(min.z, max.z), w.clamp(min.w, max.w))
+
+data class CylindricalVector(
+ val radius: Double = 1.0,
+ val angle: Angle = Angle.ZERO,
+ val y: Double = 0.0,
+) {
+ fun toVector3(): Vector3F = toCartesian(this).toFloat()
+
+ companion object {
+ fun fromCartesian(v: Vector3F): CylindricalVector = fromCartesian(v.x, v.y, v.z)
+ fun fromCartesian(v: Vector3D): CylindricalVector = fromCartesian(v.x, v.y, v.z)
+ inline fun fromCartesian(x: Number, y: Number, z: Number): CylindricalVector = fromCartesian(x.toDouble(), y.toDouble(), z.toDouble())
+ fun fromCartesian(x: Double, y: Double, z: Double): CylindricalVector = CylindricalVector(
+ radius = sqrt(x * x + z * z),
+ angle = Angle.atan2(x, z),
+ y = y,
+ )
+
+ fun toCartesian(c: CylindricalVector): Vector3D = toCartesian(c.radius, c.angle, c.y)
+ fun toCartesian(radius: Double, angle: Angle, y: Double): Vector3D = Vector3D(
+ x = radius * sin(angle),
+ y = y,
+ z = radius * cos(angle),
+ )
+ }
+}
+
+fun Vector3F.toCylindrical(): CylindricalVector = CylindricalVector.fromCartesian(this)
diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt b/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt
new file mode 100644
index 00000000..0a94709f
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt
@@ -0,0 +1,41 @@
+package com.icegps.math.geometry
+
+typealias PointInt = Vector2I
+
+data class Vector3I(val x: Int, val y: Int, val z: Int)
+data class Vector4I(val x: Int, val y: Int, val z: Int, val w: Int)
+
+//@KormaValueApi
+data class Vector2I(val x: Int, val y: Int) {
+ //operator fun component1(): Int = x
+ //operator fun component2(): Int = y
+ //fun copy(x: Int = this.x, y: Int = this.y): Vector2Int = Vector2Int(x, y)
+
+//inline class Vector2Int(internal val raw: Int2Pack) {
+
+ companion object {
+ val ZERO = Vector2I(0, 0)
+
+ fun compare(lx: Int, ly: Int, rx: Int, ry: Int): Int {
+ val ret = ly.compareTo(ry)
+ return if (ret == 0) lx.compareTo(rx) else ret
+ }
+ }
+
+ //val x: Int get() = raw.i0
+ //val y: Int get() = raw.i1
+
+ constructor() : this(0, 0)
+ //constructor(x: Int, y: Int) : this(int2PackOf(x, y))
+
+ operator fun plus(that: Vector2I): Vector2I = Vector2I(this.x + that.x, this.y + that.y)
+ operator fun minus(that: Vector2I): Vector2I = Vector2I(this.x - that.x, this.y - that.y)
+ operator fun times(that: Vector2I): Vector2I = Vector2I(this.x * that.x, this.y * that.y)
+ operator fun div(that: Vector2I): Vector2I = Vector2I(this.x / that.x, this.y / that.y)
+ operator fun rem(that: Vector2I): Vector2I = Vector2I(this.x % that.x, this.y % that.y)
+
+ override fun toString(): String = "($x, $y)"
+}
+
+fun Vector2I.toFloat(): Vector2F = Vector2F(x, y)
+fun Vector2I.toDouble(): Vector2D = Vector2D(x, y)
diff --git a/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt
new file mode 100644
index 00000000..0152bd90
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt
@@ -0,0 +1,15 @@
+package com.icegps.math.geometry.shape
+
+import com.icegps.math.geometry.*
+
+interface SimpleShape2D {
+ val closed: Boolean
+ val area: Double
+ val perimeter: Double
+ val center: Point
+ fun distance(p: Point): Double = projectedPoint(p).distanceTo(p)
+ fun normalVectorAt(p: Point): Vector2D = (p - projectedPoint(p)).normalized
+ fun projectedPoint(p: Point): Point
+ fun containsPoint(p: Point): Boolean
+ fun getBounds(): Rectangle
+}
\ No newline at end of file
diff --git a/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt
new file mode 100644
index 00000000..735824ed
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt
@@ -0,0 +1,8 @@
+package com.icegps.math.geometry.shape
+
+import com.icegps.math.geometry.*
+
+interface SimpleShape3D {
+ val center: Vector3F
+ val volume: Float
+}
diff --git a/math/src/main/java/com/icegps/math/interpolation/Easing.kt b/math/src/main/java/com/icegps/math/interpolation/Easing.kt
new file mode 100644
index 00000000..17194c60
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/interpolation/Easing.kt
@@ -0,0 +1,44 @@
+package com.icegps.math.interpolation
+
+@Suppress("unused")
+fun interface Easing {
+ operator fun invoke(it: Float): Float
+ operator fun invoke(it: Double): Double = invoke(it.toFloat()).toDouble()
+ operator fun invoke(it: Ratio): Ratio = Ratio(invoke(it.toFloat()).toDouble())
+
+ companion object {
+ operator fun invoke(name: () -> String, block: (Float) -> Float): Easing {
+ return object : Easing {
+ override fun invoke(it: Float): Float = block(it)
+ override fun toString(): String = name()
+ }
+ }
+
+ fun steps(steps: Int, easing: Easing): Easing = Easing({ "steps($steps, $easing)" }) {
+ easing((it * steps).toInt().toFloat() / steps)
+ }
+ fun cubic(f: (t: Float, b: Float, c: Float, d: Float) -> Float): Easing = Easing { f(it, 0f, 1f, 1f) }
+ fun combine(start: Easing, end: Easing): Easing = Easing { combine(it, start, end) }
+ inline fun combine(it: Float, start: Easing, end: Easing): Float =
+ if (it < .5f) .5f * start(it * 2f) else .5f * end((it - .5f) * 2f) + .5f
+
+ val LINEAR = Easing { it }
+ val SMOOTH = Easing { it * it * (3 - 2 * it) }
+ }
+}
+
+
+interface Interpolable {
+ fun interpolateWith(ratio: Ratio, other: T): T
+}
+
+interface MutableInterpolable {
+ fun setToInterpolated(ratio: Ratio, l: T, r: T): T
+}
+
+fun Ratio.interpolate(l: Float, r: Float): Float = (l + (r - l) * this.toFloat())
+fun Ratio.interpolate(l: Double, r: Double): Double = (l + (r - l) * this.toDouble())
+fun Ratio.interpolate(l: Ratio, r: Ratio): Ratio = (l + (r - l) * this)
+fun Ratio.interpolate(l: Int, r: Int): Int = (l + (r - l) * this.toDouble()).toInt()
+fun Ratio.interpolate(l: Long, r: Long): Long = (l + (r - l) * this.toDouble()).toLong()
+fun > Ratio.interpolate(l: T, r: T): T = l.interpolateWith(this, r)
diff --git a/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt b/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt
new file mode 100644
index 00000000..197c7a12
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt
@@ -0,0 +1,8 @@
+package com.icegps.math.interpolation
+
+import com.icegps.math.geometry.Vector2D
+import com.icegps.math.geometry.Vector2F
+
+
+fun Ratio.interpolate(l: Vector2D, r: Vector2D): Vector2D = Vector2D(interpolate(l.x, r.x), interpolate(l.y, r.y))
+fun Ratio.interpolate(l: Vector2F, r: Vector2F): Vector2F = Vector2F(interpolate(l.x, r.x), interpolate(l.y, r.y))
diff --git a/math/src/main/java/com/icegps/math/interpolation/Ratio.kt b/math/src/main/java/com/icegps/math/interpolation/Ratio.kt
new file mode 100644
index 00000000..c915d73c
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/interpolation/Ratio.kt
@@ -0,0 +1,109 @@
+package com.icegps.math.interpolation
+
+import com.icegps.math.*
+import kotlin.math.*
+
+//inline class Ratio(val valueD: Double) : Comparable {
+// constructor(ratio: Float) : this(ratio.toDouble())
+// val value: Double get() = valueD
+// val valueF: Float get() = value.toFloat()
+inline class Ratio(val value: Double) : Comparable {
+ constructor(ratio: Float) : this(ratio.toDouble())
+
+ fun toFloat(): Float = value.toFloat()
+ fun toDouble(): Double = value.toDouble()
+
+ constructor(value: Int, maximum: Int) : this(value.toFloat() / maximum.toFloat())
+ constructor(value: Float, maximum: Float) : this(value / maximum)
+ constructor(value: Double, maximum: Double) : this(value / maximum)
+
+ operator fun unaryPlus(): Ratio = Ratio(+this.value)
+ operator fun unaryMinus(): Ratio = Ratio(-this.value)
+ operator fun plus(that: Ratio): Ratio = Ratio(this.value + that.value)
+ operator fun minus(that: Ratio): Ratio = Ratio(this.value - that.value)
+
+ operator fun times(that: Ratio): Ratio = Ratio(this.value * that.value)
+ operator fun div(that: Ratio): Ratio = Ratio(this.value / that.value)
+ operator fun times(that: Double): Double = (this.value * that)
+ operator fun div(that: Double): Double = (this.value / that)
+
+ val absoluteValue: Ratio get() = Ratio(value.absoluteValue)
+ val clamped: Ratio get() = Ratio(value.clamp01())
+
+ fun convertToRange(min: Float, max: Float): Float = this.toFloat().convertRange(0f, 1f, min, max)
+ fun convertToRange(min: Double, max: Double): Double = this.toDouble().convertRange(0.0, 1.0, min, max)
+ fun convertToRange(min: Ratio, max: Ratio): Ratio = Ratio(this.toDouble().convertRange(0.0, 1.0, min.toDouble(), max.toDouble()))
+
+ override fun compareTo(other: Ratio): Int = value.compareTo(other.value)
+
+ fun isNaN(): Boolean = value.isNaN()
+
+ override fun toString(): String = "$value"
+
+ companion object {
+ val ZERO = Ratio(0.0)
+ val QUARTER = Ratio(.25)
+ val HALF = Ratio(.5)
+ val ONE = Ratio(1.0)
+ val NaN = Ratio(Float.NaN)
+
+ inline fun fromValueInRange(value: Number, min: Number, max: Number): Ratio =
+ value.toDouble().convertRange(min.toDouble(), max.toDouble(), 0.0, 1.0).toRatio()
+
+ inline fun fromValueInRangeClamped(value: Number, min: Number, max: Number): Ratio =
+ value.toDouble().convertRangeClamped(min.toDouble(), max.toDouble(), 0.0, 1.0).toRatio()
+
+ inline fun forEachRatio(steps: Int, include0: Boolean = true, include1: Boolean = true, block: (ratio: Ratio) -> Unit) {
+ val NS = steps - 1
+ val NSd = NS.toDouble()
+ val start = if (include0) 0 else 1
+ val end = if (include1) NS else NS - 1
+ for (n in start..end) {
+ val ratio = n.toFloat() / NSd
+ block(ratio.toRatio())
+ }
+ }
+ }
+}
+
+inline operator fun Float.times(ratio: Ratio): Float = (this * ratio.value).toFloat()
+inline operator fun Double.times(ratio: Ratio): Double = this * ratio.value
+inline operator fun Int.times(ratio: Ratio): Double = this.toDouble() * ratio.value
+inline operator fun Float.div(ratio: Ratio): Float = (this / ratio.value).toFloat()
+inline operator fun Double.div(ratio: Ratio): Double = this / ratio.value
+inline operator fun Int.div(ratio: Ratio): Double = this.toDouble() / ratio.value
+
+inline operator fun Ratio.times(value: Ratio): Ratio = Ratio(this.value * value.value)
+
+inline operator fun Ratio.times(value: Float): Float = (this.value * value).toFloat()
+inline operator fun Ratio.times(value: Double): Double = this.value * value
+inline operator fun Ratio.div(value: Float): Float = (this.value / value).toFloat()
+inline operator fun Ratio.div(value: Double): Double = this.value / value
+
+@Deprecated("", ReplaceWith("this")) fun Ratio.toRatio(): Ratio = this
+
+inline fun Number.toRatio(): Ratio = Ratio(this.toDouble())
+fun Float.toRatio(): Ratio = Ratio(this)
+fun Double.toRatio(): Ratio = Ratio(this)
+
+inline fun Number.toRatio(max: Number): Ratio = Ratio(this.toDouble(), max.toDouble())
+fun Float.toRatio(max: Float): Ratio = Ratio(this, max)
+fun Double.toRatio(max: Double): Ratio = Ratio(this, max)
+
+fun Number.toRatioClamped(): Ratio = Ratio(this.toDouble().clamp01())
+fun Float.toRatioClamped(): Ratio = Ratio(this.clamp01())
+fun Double.toRatioClamped(): Ratio = Ratio(this.clamp01())
+
+fun Ratio.convertRange(srcMin: Ratio, srcMax: Ratio, dstMin: Ratio, dstMax: Ratio): Ratio = Ratio(this.toDouble().convertRange(srcMin.toDouble(), srcMax.toDouble(), dstMin.toDouble(), dstMax.toDouble()))
+fun Ratio.isAlmostEquals(that: Ratio, epsilon: Ratio = Ratio(0.000001)): Boolean = this.toDouble().isAlmostEquals(that.toDouble(), epsilon.toDouble())
+fun Ratio.isAlmostZero(epsilon: Ratio = Ratio(0.000001)): Boolean = this.isAlmostEquals(Ratio.ZERO, epsilon)
+fun Ratio.roundDecimalPlaces(places: Int): Ratio = Ratio(value.roundDecimalPlaces(places))
+
+fun abs(a: Ratio): Ratio = Ratio(a.value.absoluteValue)
+fun min(a: Ratio, b: Ratio): Ratio = Ratio(kotlin.math.min(a.value, b.value))
+fun max(a: Ratio, b: Ratio): Ratio = Ratio(kotlin.math.max(a.value, b.value))
+fun Ratio.clamp(min: Ratio, max: Ratio): Ratio = when {
+ this < min -> min
+ this > max -> max
+ else -> this
+}
diff --git a/math/src/main/java/com/icegps/math/range/OpenRange.kt b/math/src/main/java/com/icegps/math/range/OpenRange.kt
new file mode 100644
index 00000000..9f0c5633
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/range/OpenRange.kt
@@ -0,0 +1,8 @@
+package com.icegps.math.range
+
+class OpenRange>(val start: T, val endExclusive: T)
+
+// @TODO: Would cause conflicts with Int until Int for example
+//infix fun > T.until(other: T) = OpenRange(this, other)
+
+operator fun > OpenRange.contains(item: T) = item >= this.start && item < this.endExclusive
diff --git a/math/src/main/java/com/icegps/math/range/Ranges.kt b/math/src/main/java/com/icegps/math/range/Ranges.kt
new file mode 100644
index 00000000..2edc6ddd
--- /dev/null
+++ b/math/src/main/java/com/icegps/math/range/Ranges.kt
@@ -0,0 +1,21 @@
+@file:Suppress("PackageDirectoryMismatch")
+
+package com.icegps.math.range
+
+data class DoubleRangeExclusive(val start: Double, val endExclusive: Double) {
+ val length: Double get() = endExclusive - start
+ operator fun contains(value: Double): Boolean = value >= start && value < endExclusive
+ override fun toString(): String = "${start.toString().removeSuffix(".0")} until ${endExclusive.toString().removeSuffix(".0")}"
+}
+
+inline infix fun Double.until(endExclusive: Double): DoubleRangeExclusive = DoubleRangeExclusive(this, endExclusive)
+
+data class FloatInRange(val value: Float, val min: Float, val max: Float, val inclusive: Boolean = true)
+
+data class FloatRangeExclusive(val start: Float, val endExclusive: Float) {
+ val length: Float get() = endExclusive - start
+ operator fun contains(value: Double): Boolean = value >= start && value < endExclusive
+ override fun toString(): String = "${start.toString().removeSuffix(".0")} until ${endExclusive.toString().removeSuffix(".0")}"
+}
+
+inline infix fun Float.until(endExclusive: Float): FloatRangeExclusive = FloatRangeExclusive(this, endExclusive)
diff --git a/math/src/main/java/com/icegps/memory/Bits.kt b/math/src/main/java/com/icegps/memory/Bits.kt
new file mode 100644
index 00000000..d965b1d7
--- /dev/null
+++ b/math/src/main/java/com/icegps/memory/Bits.kt
@@ -0,0 +1,349 @@
+package com.icegps.memory
+
+import com.icegps.math.*
+import kotlin.rotateLeft as rotateLeftKotlin
+import kotlin.rotateRight as rotateRightKotlin
+
+/** Returns the bits in memory of [this] float */
+public inline fun Float.reinterpretAsInt(): Int = this.toRawBits()
+/** Returns the bits in memory of [this] float */
+public inline fun Double.reinterpretAsLong(): Long = this.toRawBits()
+
+/** Returns the float representation of [this] memory bits */
+public inline fun Int.reinterpretAsFloat(): Float = Float.fromBits(this)
+/** Returns the float representation of [this] memory bits */
+public inline fun Long.reinterpretAsDouble(): Double = Double.fromBits(this)
+
+/** Rotates [this] [bits] bits to the left */
+public fun UInt.rotateLeft(bits: Int): UInt = this.rotateLeftKotlin(bits)
+/** Rotates [this] [bits] bits to the left */
+public fun Int.rotateLeft(bits: Int): Int = this.rotateLeftKotlin(bits)
+/** Rotates [this] [bits] bits to the left */
+public fun Long.rotateLeft(bits: Int): Long = this.rotateLeftKotlin(bits)
+
+/** Rotates [this] [bits] bits to the right */
+public fun UInt.rotateRight(bits: Int): UInt = this.rotateRightKotlin(bits)
+/** Rotates [this] [bits] bits to the right */
+public fun Int.rotateRight(bits: Int): Int = this.rotateRightKotlin(bits)
+/** Rotates [this] [bits] bits to the right */
+public fun Long.rotateRight(bits: Int): Long = this.rotateRightKotlin(bits)
+
+/** Reverses the bytes of [this] [Short]: AABB -> BBAA */
+public fun Short.reverseBytes(): Short {
+ val low = ((this.toInt() ushr 0) and 0xFF)
+ val high = ((this.toInt() ushr 8) and 0xFF)
+ return ((high and 0xFF) or (low shl 8)).toShort()
+}
+
+/** Reverses the bytes of [this] [Char]: AABB -> BBAA */
+public fun Char.reverseBytes(): Char = this.code.toShort().reverseBytes().toInt().toChar()
+
+/** Reverses the bytes of [this] [Int]: AABBCCDD -> DDCCBBAA */
+public fun Int.reverseBytes(): Int {
+ val v0 = ((this ushr 0) and 0xFF)
+ val v1 = ((this ushr 8) and 0xFF)
+ val v2 = ((this ushr 16) and 0xFF)
+ val v3 = ((this ushr 24) and 0xFF)
+ return (v0 shl 24) or (v1 shl 16) or (v2 shl 8) or (v3 shl 0)
+}
+
+/** Reverses the bytes of [this] [Long]: AABBCCDDEEFFGGHH -> HHGGFFEEDDCCBBAA */
+public fun Long.reverseBytes(): Long {
+ val v0 = (this ushr 0).toInt().reverseBytes().toLong() and 0xFFFFFFFFL
+ val v1 = (this ushr 32).toInt().reverseBytes().toLong() and 0xFFFFFFFFL
+ return (v0 shl 32) or (v1 shl 0)
+}
+
+/** Reverse the bits of [this] Int: abcdef...z -> z...fedcba */
+public fun Int.reverseBits(): Int {
+ var v = this
+ v = ((v ushr 1) and 0x55555555) or ((v and 0x55555555) shl 1) // swap odd and even bits
+ v = ((v ushr 2) and 0x33333333) or ((v and 0x33333333) shl 2) // swap consecutive pairs
+ v = ((v ushr 4) and 0x0F0F0F0F) or ((v and 0x0F0F0F0F) shl 4) // swap nibbles ...
+ v = ((v ushr 8) and 0x00FF00FF) or ((v and 0x00FF00FF) shl 8) // swap bytes
+ v = ((v ushr 16) and 0x0000FFFF) or ((v and 0x0000FFFF) shl 16) // swap 2-byte long pairs
+ return v
+}
+
+/** Returns the number of leading zeros of the bits of [this] integer */
+public inline fun Int.countLeadingZeros(): Int = this.countLeadingZeroBits()
+
+/** Returns the number of trailing zeros of the bits of [this] integer */
+public fun Int.countTrailingZeros(): Int = this.countTrailingZeroBits()
+
+/** Returns the number of leading ones of the bits of [this] integer */
+public fun Int.countLeadingOnes(): Int = this.inv().countLeadingZeros()
+
+/** Returns the number of trailing ones of the bits of [this] integer */
+public fun Int.countTrailingOnes(): Int = this.inv().countTrailingZeros()
+
+/** Takes n[bits] of [this] [Int], and extends the last bit, creating a plain [Int] in one's complement */
+public fun Int.signExtend(bits: Int): Int = (this shl (32 - bits)) shr (32 - bits) // Int.SIZE_BITS
+/** Takes n[bits] of [this] [Long], and extends the last bit, creating a plain [Long] in one's complement */
+public fun Long.signExtend(bits: Int): Long = (this shl (64 - bits)) shr (64 - bits) // Long.SIZE_BITS
+
+/** Creates an [Int] with [this] bits set to 1 */
+public fun Int.mask(): Int = (1 shl this) - 1
+/** Creates a [Long] with [this] bits set to 1 */
+public fun Long.mask(): Long = (1L shl this.toInt()) - 1L
+
+/** Creates an [Int] with [this] bits set to 1, displaced [offset] bits */
+public fun Int.mask(offset: Int): Int = mask() shl offset
+/** Creates a [Long] with [this] bits set to 1, displaced [offset] bits */
+public fun Long.mask(offset: Int): Long = mask() shl offset
+
+inline class IntMaskRange private constructor(val raw: Int) {
+ val offset: Int get() = raw.extract8(0)
+ val size: Int get() = raw.extract8(8)
+ fun toMask(): Int = size.mask(offset)
+
+ override fun toString(): String = "IntMaskRange(offset=$offset, size=$size)"
+
+ fun extract(value: Int): Int = value.extract(offset, size)
+ fun extractSigned(value: Int, signed: Boolean = true): Int = value.extractSigned(offset, size, signed)
+
+ companion object {
+ fun fromRange(offset: Int, size: Int): IntMaskRange = IntMaskRange(0.insert8(offset, 0).insert8(size, 8))
+ fun fromMask(mask: Int): IntMaskRange {
+ if (mask == 0) return IntMaskRange(0)
+ val offset = mask.countTrailingZeroBits()
+ val size = (32 - mask.countLeadingZeroBits()) - offset
+ return fromRange(offset, size)
+ }
+ }
+ operator fun component1(): Int = offset
+ operator fun component2(): Int = size
+}
+
+fun Int.extractMaskRange(): IntMaskRange = IntMaskRange.fromMask(this)
+
+//fun Int.getBit(offset: Int): Boolean = ((this ushr offset) and 1) != 0
+//fun Int.getBits(offset: Int, count: Int): Int = (this ushr offset) and count.mask()
+
+/** Extracts [count] bits at [offset] from [this] [Int] */
+public fun Int.extract(offset: Int, count: Int): Int = (this ushr offset) and count.mask()
+/** Extracts a bits at [offset] from [this] [Int] (returning a [Boolean]) */
+inline fun Int.extract(offset: Int): Boolean = extract1(offset) != 0
+/** Extracts a bits at [offset] from [this] [Int] (returning a [Boolean]) */
+inline fun Int.extractBool(offset: Int): Boolean = extract1(offset) != 0
+/** Extracts 1 bit at [offset] from [this] [Int] */
+inline fun Int.extract1(offset: Int): Int = (this ushr offset) and 0b1
+/** Extracts 2 bits at [offset] from [this] [Int] */
+inline fun Int.extract2(offset: Int): Int = (this ushr offset) and 0b11
+/** Extracts 3 bits at [offset] from [this] [Int] */
+inline fun Int.extract3(offset: Int): Int = (this ushr offset) and 0b111
+/** Extracts 4 bits at [offset] from [this] [Int] */
+inline fun Int.extract4(offset: Int): Int = (this ushr offset) and 0b1111
+/** Extracts 5 bits at [offset] from [this] [Int] */
+inline fun Int.extract5(offset: Int): Int = (this ushr offset) and 0b11111
+/** Extracts 6 bits at [offset] from [this] [Int] */
+inline fun Int.extract6(offset: Int): Int = (this ushr offset) and 0b111111
+/** Extracts 7 bits at [offset] from [this] [Int] */
+inline fun Int.extract7(offset: Int): Int = (this ushr offset) and 0b1111111
+/** Extracts 8 bits at [offset] from [this] [Int] */
+inline fun Int.extract8(offset: Int): Int = (this ushr offset) and 0b11111111
+/** Extracts 9 bits at [offset] from [this] [Int] */
+inline fun Int.extract9(offset: Int): Int = (this ushr offset) and 0b111111111
+/** Extracts 10 bits at [offset] from [this] [Int] */
+inline fun Int.extract10(offset: Int): Int = (this ushr offset) and 0b1111111111
+/** Extracts 11 bits at [offset] from [this] [Int] */
+inline fun Int.extract11(offset: Int): Int = (this ushr offset) and 0b11111111111
+/** Extracts 12 bits at [offset] from [this] [Int] */
+inline fun Int.extract12(offset: Int): Int = (this ushr offset) and 0b111111111111
+/** Extracts 13 bits at [offset] from [this] [Int] */
+inline fun Int.extract13(offset: Int): Int = (this ushr offset) and 0b1111111111111
+/** Extracts 14 bits at [offset] from [this] [Int] */
+inline fun Int.extract14(offset: Int): Int = (this ushr offset) and 0b11111111111111
+/** Extracts 15 bits at [offset] from [this] [Int] */
+inline fun Int.extract15(offset: Int): Int = (this ushr offset) and 0b111111111111111
+/** Extracts 16 bits at [offset] from [this] [Int] */
+inline fun Int.extract16(offset: Int): Int = (this ushr offset) and 0b1111111111111111
+/** Extracts 17 bits at [offset] from [this] [Int] */
+inline fun Int.extract17(offset: Int): Int = (this ushr offset) and 0b11111111111111111
+/** Extracts 18 bits at [offset] from [this] [Int] */
+inline fun Int.extract18(offset: Int): Int = (this ushr offset) and 0b111111111111111111
+/** Extracts 19 bits at [offset] from [this] [Int] */
+inline fun Int.extract19(offset: Int): Int = (this ushr offset) and 0b1111111111111111111
+/** Extracts 20 bits at [offset] from [this] [Int] */
+inline fun Int.extract20(offset: Int): Int = (this ushr offset) and 0b11111111111111111111
+/** Extracts 21 bits at [offset] from [this] [Int] */
+inline fun Int.extract21(offset: Int): Int = (this ushr offset) and 0b111111111111111111111
+/** Extracts 22 bits at [offset] from [this] [Int] */
+inline fun Int.extract22(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111
+/** Extracts 23 bits at [offset] from [this] [Int] */
+inline fun Int.extract23(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111
+/** Extracts 24 bits at [offset] from [this] [Int] */
+inline fun Int.extract24(offset: Int): Int = (this ushr offset) and 0xFFFFFF
+/** Extracts 25 bits at [offset] from [this] [Int] */
+inline fun Int.extract25(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111
+/** Extracts 26 bits at [offset] from [this] [Int] */
+inline fun Int.extract26(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111111
+/** Extracts 27 bits at [offset] from [this] [Int] */
+inline fun Int.extract27(offset: Int): Int = (this ushr offset) and 0b111111111111111111111111111
+/** Extracts 28 bits at [offset] from [this] [Int] */
+inline fun Int.extract28(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111111
+/** Extracts 29 bits at [offset] from [this] [Int] */
+inline fun Int.extract29(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111111111
+/** Extracts 30 bits at [offset] from [this] [Int] */
+inline fun Int.extract30(offset: Int): Int = (this ushr offset) and 0b111111111111111111111111111111
+/** Extracts 31 bits at [offset] from [this] [Int] */
+inline fun Int.extract31(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111111111
+/** Extracts 32 bits at [offset] from [this] [Int] */
+inline fun Int.extract32(offset: Int): Int = (this ushr offset) and -1
+
+
+/** Extracts [count] bits at [offset] from [this] [Int] sign-extending its result if [signed] is set to true */
+public fun Int.extractSigned(offset: Int, count: Int, signed: Boolean): Int = if (signed) extractSigned(offset, count) else extract(offset, count)
+
+/** Extracts [count] bits at [offset] from [this] [Int] sign-extending its result */
+public fun Int.extractSigned(offset: Int, count: Int): Int = ((this ushr offset) and count.mask()).signExtend(count)
+/** Extracts 8 bits at [offset] from [this] [Int] sign-extending its result */
+public fun Int.extract8Signed(offset: Int): Int = (this ushr offset).toByte().toInt()
+/** Extracts 16 bits at [offset] from [this] [Int] sign-extending its result */
+public fun Int.extract16Signed(offset: Int): Int = (this ushr offset).toShort().toInt()
+
+/** Extracts 8 bits at [offset] from [this] [Int] as [Byte] */
+public fun Int.extractByte(offset: Int): Byte = (this ushr offset).toByte()
+/** Extracts 16 bits at [offset] from [this] [Int] as [Short] */
+public fun Int.extractShort(offset: Int): Short = (this ushr offset).toShort()
+
+/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..[scale] */
+public fun Int.extractScaled(offset: Int, count: Int, scale: Int): Int = (extract(offset, count) * scale) / count.mask()
+/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0.0..1.0 */
+public fun Int.extractScaledf01(offset: Int, count: Int): Float = extract(offset, count).toFloat() / count.mask().toFloat()
+
+/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..0xFF */
+public fun Int.extractScaledFF(offset: Int, count: Int): Int = extractScaled(offset, count, 0xFF)
+/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..0xFF (if there are 0 bits, returns [default]) */
+public fun Int.extractScaledFFDefault(offset: Int, count: Int, default: Int): Int =
+ if (count == 0) default else extractScaled(offset, count, 0xFF)
+
+/** Replaces [this] bits from [offset] to [offset]+[count] with [value] and returns the result of doing such replacement */
+public fun Int.insert(value: Int, offset: Int, count: Int): Int {
+ val mask = count.mask() shl offset
+ val ovalue = (value shl offset) and mask
+ return (this and mask.inv()) or ovalue
+}
+
+public fun Int.insertNoClear(value: Int, offset: Int, count: Int): Int {
+ return this or ((value and count.mask()) shl offset)
+}
+
+public fun Int.clear(offset: Int, count: Int): Int {
+ return (this and (count.mask() shl offset).inv())
+}
+
+public fun Int.insert1(value: Int, offset: Int): Int = insertMask(value, offset, 0b1)
+public fun Int.insert2(value: Int, offset: Int): Int = insertMask(value, offset, 0b11)
+public fun Int.insert3(value: Int, offset: Int): Int = insertMask(value, offset, 0b111)
+public fun Int.insert4(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111)
+public fun Int.insert5(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111)
+public fun Int.insert6(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111)
+public fun Int.insert7(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111)
+public fun Int.insert8(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111)
+public fun Int.insert9(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111)
+public fun Int.insert10(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111)
+public fun Int.insert11(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111)
+public fun Int.insert12(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111)
+public fun Int.insert13(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111)
+public fun Int.insert14(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111)
+public fun Int.insert15(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111)
+public fun Int.insert16(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111)
+public fun Int.insert17(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111)
+public fun Int.insert18(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111)
+public fun Int.insert19(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111)
+public fun Int.insert20(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111)
+public fun Int.insert21(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111)
+public fun Int.insert22(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111)
+public fun Int.insert23(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111)
+public fun Int.insert24(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111)
+public fun Int.insert25(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111)
+public fun Int.insert26(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111111)
+public fun Int.insert27(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111111)
+public fun Int.insert28(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111111)
+public fun Int.insert29(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111111111)
+public fun Int.insert30(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111111111)
+public fun Int.insert31(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111111111)
+public fun Int.insert32(value: Int, offset: Int): Int = insertMask(value, offset, -1)
+
+/** Fast Insert: do not clear bits, assume affecting bits are 0 */
+public fun Int.finsert(value: Int, offset: Int): Int = this or (value shl offset)
+public fun Int.finsert24(value: Int, offset: Int): Int = this or ((value and 0xFFFFFF) shl offset)
+public fun Int.finsert16(value: Int, offset: Int): Int = this or ((value and 0xFFFF) shl offset)
+public fun Int.finsert12(value: Int, offset: Int): Int = this or ((value and 0xFFF) shl offset)
+public fun Int.finsert8(value: Int, offset: Int): Int = this or ((value and 0xFF) shl offset)
+public fun Int.finsert7(value: Int, offset: Int): Int = this or ((value and 0b1111111) shl offset)
+public fun Int.finsert6(value: Int, offset: Int): Int = this or ((value and 0b111111) shl offset)
+public fun Int.finsert5(value: Int, offset: Int): Int = this or ((value and 0b11111) shl offset)
+public fun Int.finsert4(value: Int, offset: Int): Int = this or ((value and 0b1111) shl offset)
+public fun Int.finsert3(value: Int, offset: Int): Int = this or ((value and 0b111) shl offset)
+public fun Int.finsert2(value: Int, offset: Int): Int = this or ((value and 0b11) shl offset)
+public fun Int.finsert1(value: Int, offset: Int): Int = this or ((value and 0b1) shl offset)
+public fun Int.finsert(value: Boolean, offset: Int): Int = finsert(value.toInt(), offset)
+
+inline fun Int.insertMask(value: Int, offset: Int, mask: Int): Int {
+ return (this and (mask shl offset).inv()) or ((value and mask) shl offset)
+}
+/** Replaces 1 bit at [offset] with [value] and returns the result of doing such replacement */
+public fun Int.insert(value: Boolean, offset: Int): Int {
+ val bits = (1 shl offset)
+ return if (value) this or bits else this and bits.inv()
+}
+
+public fun Int.insertScaled(value: Int, offset: Int, count: Int, scale: Int): Int = insert((value * count.mask()) / scale, offset, count)
+public fun Int.insertScaledFF(value: Int, offset: Int, count: Int): Int = if (count == 0) this else this.insertScaled(value, offset, count, 0xFF)
+/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0.0..1.0 */
+public fun Int.insertScaledf01(value: Float, offset: Int, count: Int): Int = this.insert((value.coerceIn(0f, 1f) * offset.mask()).toInt(), offset, count)
+
+
+/** Check if [this] has all the bits set in [bits] set */
+public infix fun Int.hasFlags(bits: Int): Boolean = (this and bits) == bits
+public infix fun Int.hasBits(bits: Int): Boolean = (this and bits) == bits
+
+/** Check if a specific bit at [index] is set */
+public infix fun Int.hasBitSet(index: Int): Boolean = ((this ushr index) and 1) != 0
+
+public infix fun Long.hasFlags(bits: Long): Boolean = (this and bits) == bits
+public infix fun Long.hasBits(bits: Long): Boolean = (this and bits) == bits
+
+/** Creates an integer with only bit [bit] set */
+public fun bit(bit: Int): Int = 1 shl bit
+
+/** Returns the integer [this] without the [bits] set */
+public fun Int.unsetBits(bits: Int): Int = this and bits.inv()
+
+/** Returns the integer [this] with the [bits] set */
+public fun Int.setBits(bits: Int): Int = this or bits
+
+/** Returns the integer [this] with the [bits] set or unset depending on the [set] parameter */
+public fun Int.setBits(bits: Int, set: Boolean): Int = if (set) setBits(bits) else unsetBits(bits)
+
+public fun Int.without(bits: Int): Int = this and bits.inv()
+public fun Int.with(bits: Int): Int = this or bits
+
+public fun Long.without(bits: Long): Long = this and bits.inv()
+public fun Long.with(bits: Long): Long = this or bits
+
+/** Get high 32-bits of this Long */
+val Long.high: Int get() = (this ushr 32).toInt()
+/** Get low 32-bits of this Long */
+val Long.low: Int get() = this.toInt()
+
+/** Get high 32-bits of this Long */
+val Long._high: Int get() = (this ushr 32).toInt()
+/** Get low 32-bits of this Long */
+val Long._low: Int get() = this.toInt()
+
+inline fun Long.Companion.fromLowHigh(low: Int, high: Int): Long = (low.toLong() and 0xFFFFFFFFL) or (high.toLong() shl 32)
+
+inline fun Int.fastForEachOneBits(block: (Int) -> Unit) {
+ var value = this
+ var index = 0
+ while (value != 0) {
+ val shift = value.countTrailingZeroBits()
+ index += shift
+ if (index < 32) block(index)
+ value = value ushr (shift + 1)
+ index++
+ }
+}
diff --git a/math/src/main/java/com/icegps/memory/DoubleBits.kt b/math/src/main/java/com/icegps/memory/DoubleBits.kt
new file mode 100644
index 00000000..87a800a4
--- /dev/null
+++ b/math/src/main/java/com/icegps/memory/DoubleBits.kt
@@ -0,0 +1,47 @@
+package com.icegps.memory
+
+// S | EEEEEEEEEEE | FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+// S=1
+// E=11
+// F=52
+
+fun Double.toStringInfo() = buildString(128) {
+ append(this@toStringInfo)
+ append(" = Double.fromParts(")
+ append("sign=")
+ append(this@toStringInfo.bitsSign)
+ append(", exponent=0b")
+ append(this@toStringInfo.bitsExponent.toString(2).padStart(11, '0'))
+ append(", mantissa=0b")
+ //append(this@toStringInfo.bitsMantissaLong.toString(2).padStart(52, '0'))
+ append(this@toStringInfo.bitsMantissaHigh.toString(2).padStart(20, '0'))
+ append(this@toStringInfo.bitsMantissaLow.toString(2).padStart(32, '0'))
+ append(")")
+}
+
+private const val TWO_POW_32_DOUBLE = 4294967296.0
+val Double.Companion.TWO_POW_32 get() = TWO_POW_32_DOUBLE
+
+fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissa: Double): Double = fromParts(sign, exponent, (mantissa % TWO_POW_32_DOUBLE).toInt(), (mantissa / TWO_POW_32_DOUBLE).toInt())
+fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissa: Long): Double = fromParts(sign, exponent, mantissa.low, mantissa.high)
+fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissaLow: Int, mantissaHigh: Int): Double = fromLowHigh(mantissaLow, mantissaHigh.insert12(exponent, 20).insert1(sign, 31))
+
+fun Double.Companion.fromLowHigh(low: Int, high: Int): Double = fromLowHighBitsSlow(low, high)
+inline fun Double.getLowHighBits(block: (low: Int, high: Int) -> T): T = getLowHighBitsSlow(block)
+/** Bit-wise equals without considering NaNs */
+fun Double.equalsRaw(other: Double): Boolean = equalsRawSlow(other)
+val Double.lowBits: Int get() = lowSlow
+val Double.highBits: Int get() = highSlow
+
+val Double.bitsSign: Int get() = highBits.extract1(31)
+val Double.bitsExponent: Int get() = highBits.extract11(20)
+val Double.bitsMantissaHigh: Int get() = highBits.extract20(0)
+val Double.bitsMantissaLow: Int get() = lowBits
+val Double.bitsMantissaDouble: Double get() = bitsMantissaLow.toDouble() + bitsMantissaHigh.toDouble() * TWO_POW_32_DOUBLE
+val Double.bitsMantissaLong: Long get() = Long.fromLowHigh(bitsMantissaLow, bitsMantissaHigh)
+
+@PublishedApi internal fun Double.Companion.fromLowHighBitsSlow(low: Int, high: Int): Double = Double.fromBits(Long.fromLowHigh(low, high))
+@PublishedApi internal inline fun Double.getLowHighBitsSlow(block: (low: Int, high: Int) -> T): T = block(lowSlow, highSlow)
+@PublishedApi internal inline fun Double.equalsRawSlow(other: Double): Boolean = this.reinterpretAsLong().equals(other.reinterpretAsLong())
+@PublishedApi internal val Double.lowSlow: Int get() = this.reinterpretAsLong().low
+@PublishedApi internal val Double.highSlow: Int get() = this.reinterpretAsLong().high
diff --git a/math/src/main/java/com/icegps/memory/Int64.kt b/math/src/main/java/com/icegps/memory/Int64.kt
new file mode 100644
index 00000000..27f79302
--- /dev/null
+++ b/math/src/main/java/com/icegps/memory/Int64.kt
@@ -0,0 +1,194 @@
+package com.icegps.memory
+
+import kotlin.contracts.*
+
+inline class Int64Array(val raw: DoubleArray) : Iterable {
+ inline val indices: IntRange get() = raw.indices
+
+ constructor(size: Int, value: Int64 = Int64.ZERO) : this(DoubleArray(size) { value.raw })
+ companion object {
+ inline operator fun invoke(size: Int, gen: (Int) -> Int64): Int64Array = Int64Array(DoubleArray(size) { gen(it).raw })
+ }
+
+ inline val size: Int get() = raw.size
+ inline operator fun get(index: Int): Int64 = Int64.fromRaw(raw[index])
+ inline operator fun set(index: Int, value: Int64) { raw[index] = value.raw }
+ override fun iterator(): Iterator = object : Iterator {
+ var index = 0
+ override fun hasNext(): Boolean = index < raw.size
+ override fun next(): Int64 = this@Int64Array[index].also { index++ }
+ }
+
+ override fun toString(): String = "IntArray64($size)"
+}
+
+inline fun int64ArrayOf(vararg values: T): Int64Array = Int64Array(values.size) { values[it] }
+inline fun int64ArrayOf(vararg values: Int): Int64Array = Int64Array(values.size) { values[it].toInt64() }
+inline fun int64ArrayOf(vararg values: Long): Int64Array = Int64Array(values.size) { values[it].toInt64() }
+
+fun Int64Array.copyOf(newSize: Int = this.size): Int64Array = Int64Array(raw.copyOf(newSize))
+fun Int64Array.copyOfRange(fromIndex: Int, toIndex: Int): Int64Array = Int64Array(raw.copyOfRange(fromIndex, toIndex))
+public fun Int64Array.getOrNull(index: Int): Int64? = if (index in indices) get(index) else null
+//@kotlin.internal.InlineOnly
+@OptIn(ExperimentalContracts::class)
+public inline fun Int64Array.getOrElse(index: Int, defaultValue: (Int) -> Int64): Int64 {
+ contract { callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) }
+ return if (index in indices) get(index) else defaultValue(index)
+}
+
+infix fun Int64Array?.contentEquals(other: Int64Array?): Boolean = this?.raw.contentEquals(other?.raw)
+fun Int64Array?.contentHashCode(): Int = this?.raw.contentHashCode()
+fun Int64Array?.contentToString(): String = if (this == null) "null" else "[" + this.raw.joinToString(", ") { it.toString() } + "]"
+
+/**
+ * Allocation-less Long implementation that uses a Double with reinterpreted values
+ *
+ * IMPORTANT:
+ *
+ * Due to Kotlin not supporting [equals] in inline classes,
+ * Equality fails in some cases where Int64 represents a NaN or an Infinity.
+ * For comparing Int64, use [Int64.equalsSafe] instead.
+ */
+inline class Int64(val raw: Double) : Comparable {
+ companion object {
+ val ZERO = Int64(0, 0)
+
+ fun equals(a: Int64, b: Int64): Boolean = a.raw.equalsRaw(b.raw)
+
+ inline operator fun invoke(value: Long): Int64 = Int64(value.reinterpretAsDouble())
+ inline operator fun invoke(low: Int, high: Int): Int64 = Int64(Double.fromLowHigh(low, high))
+ inline operator fun invoke(value: Int64): Int64 = Int64(value.raw)
+ inline operator fun invoke(value: UInt): Int64 = Int64(Double.fromLowHigh(value.toInt(), 0))
+ inline operator fun invoke(value: Int): Int64 = when {
+ value < 0 -> Int64(Double.fromLowHigh(value and (1 shl 31), 1 shl 31))
+ else -> Int64(Double.fromLowHigh(value, 0))
+ }
+
+ inline fun fromRaw(value: Double) = Int64(value)
+ inline fun fromInt52(values: Double) = Int64(Double.fromParts(0, 0, values))
+
+ fun add(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 {
+ val low = low1 + low2
+ val carry = if (low < low1) 1 else 0
+ val high = high1 + high2 + carry
+ return Int64(low.toInt(), high)
+ }
+ fun sub(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 {
+ val lowDiff = low1 - low2
+ val borrow = if (low1 < low2) 1 else 0
+ val highDiff = high1 - high2 - borrow
+ return Int64(lowDiff.toInt(), highDiff)
+ }
+ // @TODO: Fix this
+ fun imul(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 {
+ if (low1 == 0u && high1 == 0) return Int64.ZERO
+ if (low2 == 0u && high2 == 0) return Int64.ZERO
+
+ /*
+ if (equalsLong(_this__u8e3s4, get_MIN_VALUE())) {
+ return if (isOdd(other)) get_MIN_VALUE() else get_ZERO()
+ } else if (equalsLong(other, get_MIN_VALUE())) {
+ return if (isOdd(_this__u8e3s4)) get_MIN_VALUE() else get_ZERO()
+ }
+ if (isNegative(_this__u8e3s4)) {
+ val tmp: Unit
+ if (isNegative(other)) {
+ tmp = multiply(negate(_this__u8e3s4), negate(other))
+ } else {
+ tmp = negate(multiply(negate(_this__u8e3s4), other))
+ }
+ return tmp
+ } else if (isNegative(other)) {
+ return negate(multiply(_this__u8e3s4, negate(other)))
+ }
+ if (lessThan(_this__u8e3s4, get_TWO_PWR_24_()) && lessThan(other, get_TWO_PWR_24_())) {
+ return fromNumber(toNumber(_this__u8e3s4) * toNumber(other))
+ }
+ val a48: Unit = _this__u8e3s4.high_1 ushr 16 or 0
+ val a32: Unit = _this__u8e3s4.high_1 and 65535
+ val a16: Unit = _this__u8e3s4.low_1 ushr 16 or 0
+ val a00: Unit = _this__u8e3s4.low_1 and 65535
+ val b48: Unit = other.high_1 ushr 16 or 0
+ val b32: Unit = other.high_1 and 65535
+ val b16: Unit = other.low_1 ushr 16 or 0
+ val b00: Unit = other.low_1 and 65535
+ var c48 = 0
+ var c32 = 0
+ var c16 = 0
+ var c00 = 0
+ c00 = c00 + imul(a00, b00) or 0
+ c16 = c16 + (c00 ushr 16 or 0) or 0
+ c00 = c00 and 65535
+ c16 = c16 + imul(a16, b00) or 0
+ c32 = c32 + (c16 ushr 16 or 0) or 0
+ c16 = c16 and 65535
+ c16 = c16 + imul(a00, b16) or 0
+ c32 = c32 + (c16 ushr 16 or 0) or 0
+ c16 = c16 and 65535
+ c32 = c32 + imul(a32, b00) or 0
+ c48 = c48 + (c32 ushr 16 or 0) or 0
+ c32 = c32 and 65535
+ c32 = c32 + imul(a16, b16) or 0
+ c48 = c48 + (c32 ushr 16 or 0) or 0
+ c32 = c32 and 65535
+ c32 = c32 + imul(a00, b32) or 0
+ c48 = c48 + (c32 ushr 16 or 0) or 0
+ c32 = c32 and 65535
+ c48 = c48 + (((imul(a48, b00) + imul(a32, b16) or 0) + imul(a16, b32) or 0) + imul(a00, b48) or 0) or 0
+ c48 = c48 and 65535
+ return Long(c16 shl 16 or c00, c48 shl 16 or c32)
+ */
+ TODO()
+ }
+ }
+
+ inline val isNegative get() = high.extract1(31) != 0
+ inline val isPositive get() = !isNegative
+ inline val isZero get() = low == 0 && high == 0
+
+ operator fun unaryPlus(): Int64 = this
+ operator fun unaryMinus(): Int64 = Int64(low, -high)
+ fun inv(): Int64 = Int64(low.inv(), high.inv())
+
+ operator fun plus(other: Int64): Int64 = add(ulow, high, other.ulow, other.high)
+ operator fun minus(other: Int64): Int64 = sub(ulow, high, other.ulow, other.high)
+ infix fun xor(other: Int64): Int64 = Int64(low xor other.low, high xor other.high)
+ infix fun and(other: Int64): Int64 = Int64(low and other.low, high and other.high)
+ infix fun or(other: Int64): Int64 = Int64(low or other.low, high or other.high)
+
+ //infix fun shl(other: Int): Int64 = Int64(low shl other, high shl other) // @TODO: Fix this
+ //infix fun shr(other: Int): Int64 = Int64(low shr other, high shr other) // @TODO: Fix this
+ //infix fun ushr(other: Int): Int64 = Int64(low ushr other, high ushr other) // @TODO: Fix this
+
+ // @TODO: SLOW (USE INTERMEDIARY LONGS)
+ infix fun shl(other: Int): Int64 = Int64(toLong() shl other)
+ infix fun shr(other: Int): Int64 = Int64(toLong() shr other)
+ infix fun ushr(other: Int): Int64 = Int64(toLong() ushr other)
+ operator fun times(other: Int64): Int64 {
+ if (this.isZero || other.isZero) return Int64.ZERO
+ return Int64(toLong() * other.toLong())
+ }
+ //operator fun times(other: Int64): Int64 = imul(ulow, high, other.ulow, other.high) // @TODO: Fix this
+ operator fun div(other: Int64): Int64 = Int64(toLong() / other.toLong())
+ operator fun rem(other: Int64): Int64 = Int64(toLong() % other.toLong())
+ override fun compareTo(other: Int64): Int = this.toLong().compareTo(other.toLong())
+ // @TODO /END SLOW (USE INTERMEDIARY LONGS)
+
+ //val int52: Double get() = raw.bitsMantissaDouble
+ inline val ulow: UInt get() = raw.lowBits.toUInt()
+ inline val low: Int get() = raw.lowBits
+ inline val high: Int get() = raw.highBits
+
+ fun equalsSafe(other: Int64): Boolean = equals(this, other)
+
+ fun toInt(): Int = if (isPositive) low and 0x7FFFFFFF else -(low and 0x7FFFFFFF)
+ inline fun toLong(): Long = raw.reinterpretAsLong()
+
+ override fun toString(): String = "${toLong()}"
+}
+
+fun Byte.toInt64(): Int64 = Int64(this.toInt())
+fun Int.toInt64(): Int64 = Int64(this)
+fun Long.toInt64(): Int64 = Int64(this)
+fun Double.toInt64(): Int64 = Int64.fromInt52(this)
+fun Number.toInt64(): Int64 = Int64(this.toLong())
diff --git a/math/src/main/java/com/icegps/number/StringExt.kt b/math/src/main/java/com/icegps/number/StringExt.kt
new file mode 100644
index 00000000..f04a64e9
--- /dev/null
+++ b/math/src/main/java/com/icegps/number/StringExt.kt
@@ -0,0 +1,71 @@
+package com.icegps.number
+
+import com.icegps.math.*
+import kotlin.math.*
+
+val Double.niceStr: String get() = niceStr(-1, zeroSuffix = false)
+fun Double.niceStr(decimalPlaces: Int, zeroSuffix: Boolean = false): String = buildString { appendNice(this@niceStr.roundDecimalPlaces(decimalPlaces), zeroSuffix = zeroSuffix && decimalPlaces > 0) }
+
+val Float.niceStr: String get() = niceStr(-1, zeroSuffix = false)
+fun Float.niceStr(decimalPlaces: Int, zeroSuffix: Boolean = false): String = buildString { appendNice(this@niceStr.roundDecimalPlaces(decimalPlaces), zeroSuffix = zeroSuffix && decimalPlaces > 0) }
+
+fun StringBuilder.appendNice(value: Double, zeroSuffix: Boolean = false): Unit {
+ when {
+ round(value).isAlmostEquals(value) -> when {
+ value >= Int.MIN_VALUE.toDouble() && value <= Int.MAX_VALUE.toDouble() -> append(round(value).toInt())
+ else -> append(round(value).toLong())
+ }
+ else -> {
+ append(value)
+ return
+ }
+ }
+ if (zeroSuffix) append(".0")
+}
+fun StringBuilder.appendNice(value: Float, zeroSuffix: Boolean = false): Unit {
+ when {
+ round(value).isAlmostEquals(value) -> when {
+ value >= Int.MIN_VALUE.toFloat() && value <= Int.MAX_VALUE.toFloat() -> append(value.toInt())
+ else -> append(value.toLong())
+ }
+ else -> {
+ append(value)
+ return
+ }
+ }
+ if (zeroSuffix) append(".0")
+}
+fun StringBuilder.appendGenericArray(size: Int, appendElement: StringBuilder.(Int) -> Unit) {
+ append("[")
+ for (n in 0 until size) {
+ if (n != 0) append(", ")
+ appendElement(n)
+ }
+ append("]")
+}
+
+//val Float.niceStr: String get() = buildString { appendNice(this@niceStr) }
+//fun Float.niceStr(decimalPlaces: Int): String = roundDecimalPlaces(decimalPlaces).niceStr
+//val Float.niceStr: String get() = buildString { appendNice(this@niceStr) }
+//fun Float.niceStr(decimalPlaces: Int): String = roundDecimalPlaces(decimalPlaces).niceStr
+
+/*
+internal fun StringBuilder.appendNice(value: Double) {
+ when {
+ round(value).isAlmostEquals(value) -> when {
+ value >= Int.MIN_VALUE.toDouble() && value <= Int.MAX_VALUE.toDouble() -> append(value.toInt())
+ else -> append(value.toLong())
+ }
+ else -> append(value)
+ }
+}
+internal fun StringBuilder.appendNice(value: Float) {
+ when {
+ round(value).isAlmostEquals(value) -> when {
+ value >= Int.MIN_VALUE.toFloat() && value <= Int.MAX_VALUE.toFloat() -> append(value.toInt())
+ else -> append(value.toLong())
+ }
+ else -> append(value)
+ }
+}
+*/
diff --git a/math/src/test/java/com/icegps/math/geometry/AngleTest.kt b/math/src/test/java/com/icegps/math/geometry/AngleTest.kt
new file mode 100644
index 00000000..4cc9d216
--- /dev/null
+++ b/math/src/test/java/com/icegps/math/geometry/AngleTest.kt
@@ -0,0 +1,22 @@
+package com.icegps.math.geometry
+
+import kotlin.test.Test
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/10/28
+ */
+class AngleTest {
+ @Test
+ fun testAngle() {
+ val angle = 90.degrees
+ println(angle)
+ println(angle.degrees)
+
+ val angle1 = 1.9.radians
+ println(angle1)
+ println(angle1.radians)
+
+ println(angle1.degrees)
+ }
+}
\ No newline at end of file
diff --git a/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt b/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt
new file mode 100644
index 00000000..ecc25ca4
--- /dev/null
+++ b/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt
@@ -0,0 +1,15 @@
+package com.icegps.math.geometry
+
+import kotlin.test.Test
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/10/19
+ */
+class EulerRotationTest {
+ @Test
+ fun testEulerRotation() {
+ val eulerRotation = EulerRotation(12.degrees, 12.degrees, 12.degrees)
+ println(eulerRotation)
+ }
+}
\ No newline at end of file
diff --git a/math/src/test/java/com/icegps/number/NiceStrTest.kt b/math/src/test/java/com/icegps/number/NiceStrTest.kt
new file mode 100644
index 00000000..cba9dc0d
--- /dev/null
+++ b/math/src/test/java/com/icegps/number/NiceStrTest.kt
@@ -0,0 +1,15 @@
+package com.icegps.number
+
+import com.icegps.math.geometry.degrees
+import kotlin.test.Test
+
+/**
+ * @author tabidachinokaze
+ * @date 2025/10/19
+ */
+class NiceStrTest {
+ @Test
+ fun testNiceStr() {
+ println((12.0 / 12.1).degrees.degrees.niceStr(2))
+ }
+}
diff --git a/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt
index 50ab0845..21644009 100644
--- a/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt
+++ b/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt
@@ -43,30 +43,30 @@ fun main() = application {
extend(Orbital())
extend {
- drawer.rotate(Vector3.Companion.UNIT_Y, seconds * 45.0 + 45.0, TransformTarget.MODEL)
- drawer.translate(0.0, 0.0, 9.0, TransformTarget.VIEW)
- drawer.shadeStyle = shadeStyle {
+ //drawer.rotate(Vector3.Companion.UNIT_Y, seconds * 45.0 + 45.0, TransformTarget.MODEL)
+ //drawer.translate(0.0, 0.0, 9.0, TransformTarget.VIEW)
+ if (true)drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb = normalize(v_viewNormal) * 0.5 + vec3(0.5);
""".trimIndent()
}
- drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
+ if (false)drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.0
- drawer.shadeStyle = shadeStyle {
+ if (false)drawer.shadeStyle = shadeStyle {
vertexTransform = """
x_projectionMatrix[3][2] -= 0.001;
""".trimIndent()
}
drawer.strokeWeight = 1.0
- drawer.paths(paths.mapIndexed { index, it ->
+ drawer.paths(paths/*.mapIndexed { index, it ->
it.sub(
0.0, cos(seconds * 0.5 + index * 0.5) * 0.5 + 0.5
)
- })
+ }*/)
}
}
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3edaf4f7..284341f6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -5,14 +5,33 @@ rootProject.name = "orx"
includeBuild("build-logic")
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
dependencyResolutionManagement {
repositories {
+ google()
mavenCentral()
mavenLocal {
content {
includeGroup("org.openrndr")
}
}
+ // Mapbox Maven repository
+ maven {
+ url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
+ }
}
versionCatalogs {
// We use a regex to get the openrndr version from the primary catalog as there is no public Gradle API to parse catalogs.
@@ -105,4 +124,10 @@ include(
"orx-module-catalog"
)
-)
\ No newline at end of file
+)
+
+include(":android")
+include(":math")
+include(":desktop")
+include(":icegps-common")
+include(":icegps-shared")
\ No newline at end of file