diff --git a/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/Engine.kt b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/Engine.kt new file mode 100644 index 0000000..0ecb429 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/Engine.kt @@ -0,0 +1,70 @@ +import com.curiouscreature.kotlin.math.* + +data class Camera( + inline var position: Float3 = Float3(), + inline var target: Float3 = Float3() +) + +class Mesh(val name: String, val triangles: Array) { + var position = Float3() + var rotation = Float3() +} + +val cube = Mesh( + "Cube", arrayOf( + + // SOUTH + Mat3(Float3(-1.0f, -1.0f, -1.0f), Float3(-1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f)), + Mat3(Float3(-1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f), Float3(1.0f, -1.0f, -1.0f)), + + // EAST + Mat3(Float3(1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f)), + Mat3(Float3(1.0f, -1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(1.0f, -1.0f, 1.0f)), + + // NORTH + Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f)), + Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(-1.0f, -1.0f, 1.0f)), + + // WEST + Mat3(Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(-1.0f, 1.0f, -1.0f)), + Mat3(Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, 1.0f, -1.0f), Float3(-1.0f, -1.0f, -1.0f)), + + // TOP + Mat3(Float3(-1.0f, 1.0f, -1.0f), Float3(-1.0f, 1.0f, 1.0f), Float3(1.0f, 1.0f, 1.0f)), + Mat3(Float3(-1.0f, 1.0f, -1.0f), Float3(1.0f, 1.0f, 1.0f), Float3(1.0f, 1.0f, -1.0f)), + + // BOTTOM + Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, -1.0f)), + Mat3(Float3(1.0f, -1.0f, 1.0f), Float3(-1.0f, -1.0f, -1.0f), Float3(1.0f, -1.0f, -1.0f)), + ) +) + +fun project(coord: Float3, transMat: Mat4): Float2 { + val (x, y, _, _) = transMat * (Float4(coord, 0f)) + return Float2(x, y) +} + +/** + * Project triangle to 2d + * this was a shot in the dark, might be a bug here. Looks ok + */ +fun Mat3.project(transMat: Mat4): List { + + val projected = transMat * Mat4(this.x, this.y, this.z) + + return listOf( + Float2(projected[0][0], projected[0][1]), + Float2(projected[1][0], projected[1][1]), + Float2(projected[2][0], projected[2][1]), + ) +} + +fun render3d(camera: Camera, mesh: Mesh): List> { + + val viewMatrix = lookAt(camera.position, camera.target) + val projectionMatrix = perspective(0.78f, 1f, 0.01f, 20f) + val worldMatrix = rotation(mesh.rotation) * translation(mesh.position) + val transformMatrix = projectionMatrix * viewMatrix * worldMatrix + return mesh.triangles.map { it.project(transformMatrix) } +} + diff --git a/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Matrix.kt b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Matrix.kt new file mode 100644 index 0000000..6f88be2 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Matrix.kt @@ -0,0 +1,521 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.curiouscreature.kotlin.math + +import kotlin.math.* + +enum class MatrixColumn { + X, Y, Z, W +} + +data class Mat2( + var x: Float2 = Float2(x = 1.0f), + var y: Float2 = Float2(y = 1.0f)) { + constructor(m: Mat2) : this(m.x.copy(), m.y.copy()) + + companion object { + fun of(vararg a: Float): Mat2 { + require(a.size >= 4) + return Mat2( + Float2(a[0], a[2]), + Float2(a[1], a[3]) + ) + } + + fun identity() = Mat2() + } + + operator fun get(column: Int) = when(column) { + 0 -> x + 1 -> y + else -> throw IllegalArgumentException("column must be in 0..1") + } + operator fun get(column: Int, row: Int) = get(column)[row] + + operator fun get(column: MatrixColumn) = when(column) { + MatrixColumn.X -> x + MatrixColumn.Y -> y + else -> throw IllegalArgumentException("column must be X or Y") + } + operator fun get(column: MatrixColumn, row: Int) = get(column)[row] + + operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1] + operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v) + + operator fun set(column: Int, v: Float2) { + this[column].xy = v + } + operator fun set(column: Int, row: Int, v: Float) { + this[column][row] = v + } + + operator fun unaryMinus() = Mat2(-x, -y) + operator fun inc(): Mat2 { + x++ + y++ + return this + } + operator fun dec(): Mat2 { + x-- + y-- + return this + } + + operator fun plus(v: Float) = Mat2(x + v, y + v) + operator fun minus(v: Float) = Mat2(x - v, y - v) + operator fun times(v: Float) = Mat2(x * v, y * v) + operator fun div(v: Float) = Mat2(x / v, y / v) + + operator fun times(m: Mat2): Mat2 { + val t = transpose(this) + return Mat2( + Float2(dot(t.x, m.x), dot(t.y, m.x)), + Float2(dot(t.x, m.y), dot(t.y, m.y)) + ) + } + + operator fun times(v: Float2): Float2 { + val t = transpose(this) + return Float2(dot(t.x, v), dot(t.y, v)) + } + + fun toFloatArray() = floatArrayOf( + x.x, y.x, + x.y, y.y + ) + + override fun toString(): String { + return """ + |${x.x} ${y.x}| + |${x.y} ${y.y}| + """.trimIndent() + } + +} + +data class Mat3( + var x: Float3 = Float3(x = 1.0f), + var y: Float3 = Float3(y = 1.0f), + var z: Float3 = Float3(z = 1.0f)) { + constructor(m: Mat3) : this(m.x.copy(), m.y.copy(), m.z.copy()) + + companion object { + fun of(vararg a: Float): Mat3 { + require(a.size >= 9) + return Mat3( + Float3(a[0], a[3], a[6]), + Float3(a[1], a[4], a[7]), + Float3(a[2], a[5], a[8]) + ) + } + + fun identity() = Mat3() + } + + operator fun get(column: Int) = when(column) { + 0 -> x + 1 -> y + 2 -> z + else -> throw IllegalArgumentException("column must be in 0..2") + } + operator fun get(column: Int, row: Int) = get(column)[row] + + operator fun get(column: MatrixColumn) = when(column) { + MatrixColumn.X -> x + MatrixColumn.Y -> y + MatrixColumn.Z -> z + else -> throw IllegalArgumentException("column must be X, Y or Z") + } + operator fun get(column: MatrixColumn, row: Int) = get(column)[row] + + operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1] + operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v) + + operator fun set(column: Int, v: Float3) { + this[column].xyz = v + } + operator fun set(column: Int, row: Int, v: Float) { + this[column][row] = v + } + + operator fun unaryMinus() = Mat3(-x, -y, -z) + operator fun inc(): Mat3 { + x++ + y++ + z++ + return this + } + operator fun dec(): Mat3 { + x-- + y-- + z-- + return this + } + + operator fun plus(v: Float) = Mat3(x + v, y + v, z + v) + operator fun minus(v: Float) = Mat3(x - v, y - v, z - v) + operator fun times(v: Float) = Mat3(x * v, y * v, z * v) + operator fun div(v: Float) = Mat3(x / v, y / v, z / v) + + operator fun times(m: Mat3): Mat3 { + val t = transpose(this) + return Mat3( + Float3(dot(t.x, m.x), dot(t.y, m.x), dot(t.z, m.x)), + Float3(dot(t.x, m.y), dot(t.y, m.y), dot(t.z, m.y)), + Float3(dot(t.x, m.z), dot(t.y, m.z), dot(t.z, m.z)) + ) + } + + operator fun times(v: Float3): Float3 { + val t = transpose(this) + return Float3(dot(t.x, v), dot(t.y, v), dot(t.z, v)) + } + + fun toFloatArray() = floatArrayOf( + x.x, y.x, z.x, + x.y, y.y, z.y, + x.z, y.z, z.z + ) + + override fun toString(): String { + return """ + |${x.x} ${y.x} ${z.x}| + |${x.y} ${y.y} ${z.y}| + |${x.z} ${y.z} ${z.z}| + """.trimIndent() + } +} + +data class Mat4( + var x: Float4 = Float4(x = 1.0f), + var y: Float4 = Float4(y = 1.0f), + var z: Float4 = Float4(z = 1.0f), + var w: Float4 = Float4(w = 1.0f)) { + constructor(right: Float3, up: Float3, forward: Float3, position: Float3 = Float3()) : + this(Float4(right), Float4(up), Float4(forward), Float4(position, 1.0f)) + constructor(m: Mat4) : this(m.x.copy(), m.y.copy(), m.z.copy(), m.w.copy()) + + companion object { + fun of(vararg a: Float): Mat4 { + require(a.size >= 16) + return Mat4( + Float4(a[0], a[4], a[8], a[12]), + Float4(a[1], a[5], a[9], a[13]), + Float4(a[2], a[6], a[10], a[14]), + Float4(a[3], a[7], a[11], a[15]) + ) + } + + fun identity() = Mat4() + } + + inline var right: Float3 + get() = x.xyz + set(value) { + x.xyz = value + } + inline var up: Float3 + get() = y.xyz + set(value) { + y.xyz = value + } + inline var forward: Float3 + get() = z.xyz + set(value) { + z.xyz = value + } + inline var position: Float3 + get() = w.xyz + set(value) { + w.xyz = value + } + + inline val scale: Float3 + get() = Float3(length(x.xyz), length(y.xyz), length(z.xyz)) + inline val translation: Float3 + get() = w.xyz + val rotation: Float3 + get() { + val x = normalize(right) + val y = normalize(up) + val z = normalize(forward) + + return when { + z.y <= -1.0f -> Float3(degrees(-HALF_PI), 0.0f, degrees(atan2( x.z, y.z))) + z.y >= 1.0f -> Float3(degrees( HALF_PI), 0.0f, degrees(atan2(-x.z, -y.z))) + else -> Float3( + degrees(-asin(z.y)), degrees(-atan2(z.x, z.z)), degrees(atan2( x.y, y.y))) + } + } + + inline val upperLeft: Mat3 + get() = Mat3(x.xyz, y.xyz, z.xyz) + + operator fun get(column: Int) = when(column) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> throw IllegalArgumentException("column must be in 0..3") + } + operator fun get(column: Int, row: Int) = get(column)[row] + + operator fun get(column: MatrixColumn) = when(column) { + MatrixColumn.X -> x + MatrixColumn.Y -> y + MatrixColumn.Z -> z + MatrixColumn.W -> w + } + operator fun get(column: MatrixColumn, row: Int) = get(column)[row] + + operator fun invoke(row: Int, column: Int) = get(column - 1)[row - 1] + operator fun invoke(row: Int, column: Int, v: Float) = set(column - 1, row - 1, v) + + operator fun set(column: Int, v: Float4) { + this[column].xyzw = v + } + operator fun set(column: Int, row: Int, v: Float) { + this[column][row] = v + } + + operator fun unaryMinus() = Mat4(-x, -y, -z, -w) + operator fun inc(): Mat4 { + x++ + y++ + z++ + w++ + return this + } + operator fun dec(): Mat4 { + x-- + y-- + z-- + w-- + return this + } + + operator fun plus(v: Float) = Mat4(x + v, y + v, z + v, w + v) + operator fun minus(v: Float) = Mat4(x - v, y - v, z - v, w - v) + operator fun times(v: Float) = Mat4(x * v, y * v, z * v, w * v) + operator fun div(v: Float) = Mat4(x / v, y / v, z / v, w / v) + + operator fun times(m: Mat4): Mat4 { + val t = transpose(this) + return Mat4( + Float4(dot(t.x, m.x), dot(t.y, m.x), dot(t.z, m.x), dot(t.w, m.x)), + Float4(dot(t.x, m.y), dot(t.y, m.y), dot(t.z, m.y), dot(t.w, m.y)), + Float4(dot(t.x, m.z), dot(t.y, m.z), dot(t.z, m.z), dot(t.w, m.z)), + Float4(dot(t.x, m.w), dot(t.y, m.w), dot(t.z, m.w), dot(t.w, m.w)) + ) + } + + operator fun times(v: Float4): Float4 { + val t = transpose(this) + return Float4(dot(t.x, v), dot(t.y, v), dot(t.z, v), dot(t.w, v)) + } + + fun toFloatArray() = floatArrayOf( + x.x, y.x, z.x, w.x, + x.y, y.y, z.y, w.y, + x.z, y.z, z.z, w.z, + x.w, y.w, z.w, w.w + ) + + override fun toString(): String { + return """ + |${x.x} ${y.x} ${z.x} ${w.x}| + |${x.y} ${y.y} ${z.y} ${w.y}| + |${x.z} ${y.z} ${z.z} ${w.z}| + |${x.w} ${y.w} ${z.w} ${w.w}| + """.trimIndent() + } +} + +fun transpose(m: Mat2) = Mat2( + Float2(m.x.x, m.y.x), + Float2(m.x.y, m.y.y) +) + +fun transpose(m: Mat3) = Mat3( + Float3(m.x.x, m.y.x, m.z.x), + Float3(m.x.y, m.y.y, m.z.y), + Float3(m.x.z, m.y.z, m.z.z) +) +fun inverse(m: Mat3): Mat3 { + val a = m.x.x + val b = m.x.y + val c = m.x.z + val d = m.y.x + val e = m.y.y + val f = m.y.z + val g = m.z.x + val h = m.z.y + val i = m.z.z + + val A = e * i - f * h + val B = f * g - d * i + val C = d * h - e * g + + val det = a * A + b * B + c * C + + return Mat3.of( + A / det, B / det, C / det, + (c * h - b * i) / det, (a * i - c * g) / det, (b * g - a * h) / det, + (b * f - c * e) / det, (c * d - a * f) / det, (a * e - b * d) / det + ) +} + +fun transpose(m: Mat4) = Mat4( + Float4(m.x.x, m.y.x, m.z.x, m.w.x), + Float4(m.x.y, m.y.y, m.z.y, m.w.y), + Float4(m.x.z, m.y.z, m.z.z, m.w.z), + Float4(m.x.w, m.y.w, m.z.w, m.w.w) +) +fun inverse(m: Mat4): Mat4 { + val result = Mat4() + + var pair0 = m.z.z * m.w.w + var pair1 = m.w.z * m.z.w + var pair2 = m.y.z * m.w.w + var pair3 = m.w.z * m.y.w + var pair4 = m.y.z * m.z.w + var pair5 = m.z.z * m.y.w + var pair6 = m.x.z * m.w.w + var pair7 = m.w.z * m.x.w + var pair8 = m.x.z * m.z.w + var pair9 = m.z.z * m.x.w + var pair10 = m.x.z * m.y.w + var pair11 = m.y.z * m.x.w + + result.x.x = pair0 * m.y.y + pair3 * m.z.y + pair4 * m.w.y + result.x.x -= pair1 * m.y.y + pair2 * m.z.y + pair5 * m.w.y + result.x.y = pair1 * m.x.y + pair6 * m.z.y + pair9 * m.w.y + result.x.y -= pair0 * m.x.y + pair7 * m.z.y + pair8 * m.w.y + result.x.z = pair2 * m.x.y + pair7 * m.y.y + pair10 * m.w.y + result.x.z -= pair3 * m.x.y + pair6 * m.y.y + pair11 * m.w.y + result.x.w = pair5 * m.x.y + pair8 * m.y.y + pair11 * m.z.y + result.x.w -= pair4 * m.x.y + pair9 * m.y.y + pair10 * m.z.y + result.y.x = pair1 * m.y.x + pair2 * m.z.x + pair5 * m.w.x + result.y.x -= pair0 * m.y.x + pair3 * m.z.x + pair4 * m.w.x + result.y.y = pair0 * m.x.x + pair7 * m.z.x + pair8 * m.w.x + result.y.y -= pair1 * m.x.x + pair6 * m.z.x + pair9 * m.w.x + result.y.z = pair3 * m.x.x + pair6 * m.y.x + pair11 * m.w.x + result.y.z -= pair2 * m.x.x + pair7 * m.y.x + pair10 * m.w.x + result.y.w = pair4 * m.x.x + pair9 * m.y.x + pair10 * m.z.x + result.y.w -= pair5 * m.x.x + pair8 * m.y.x + pair11 * m.z.x + + pair0 = m.z.x * m.w.y + pair1 = m.w.x * m.z.y + pair2 = m.y.x * m.w.y + pair3 = m.w.x * m.y.y + pair4 = m.y.x * m.z.y + pair5 = m.z.x * m.y.y + pair6 = m.x.x * m.w.y + pair7 = m.w.x * m.x.y + pair8 = m.x.x * m.z.y + pair9 = m.z.x * m.x.y + pair10 = m.x.x * m.y.y + pair11 = m.y.x * m.x.y + + result.z.x = pair0 * m.y.w + pair3 * m.z.w + pair4 * m.w.w + result.z.x -= pair1 * m.y.w + pair2 * m.z.w + pair5 * m.w.w + result.z.y = pair1 * m.x.w + pair6 * m.z.w + pair9 * m.w.w + result.z.y -= pair0 * m.x.w + pair7 * m.z.w + pair8 * m.w.w + result.z.z = pair2 * m.x.w + pair7 * m.y.w + pair10 * m.w.w + result.z.z -= pair3 * m.x.w + pair6 * m.y.w + pair11 * m.w.w + result.z.w = pair5 * m.x.w + pair8 * m.y.w + pair11 * m.z.w + result.z.w -= pair4 * m.x.w + pair9 * m.y.w + pair10 * m.z.w + result.w.x = pair2 * m.z.z + pair5 * m.w.z + pair1 * m.y.z + result.w.x -= pair4 * m.w.z + pair0 * m.y.z + pair3 * m.z.z + result.w.y = pair8 * m.w.z + pair0 * m.x.z + pair7 * m.z.z + result.w.y -= pair6 * m.z.z + pair9 * m.w.z + pair1 * m.x.z + result.w.z = pair6 * m.y.z + pair11 * m.w.z + pair3 * m.x.z + result.w.z -= pair10 * m.w.z + pair2 * m.x.z + pair7 * m.y.z + result.w.w = pair10 * m.z.z + pair4 * m.x.z + pair9 * m.y.z + result.w.w -= pair8 * m.y.z + pair11 * m.z.z + pair5 * m.x.z + + val determinant = m.x.x * result.x.x + m.y.x * result.x.y + m.z.x * result.x.z + m.w.x * result.x.w + + return result / determinant +} + +fun scale(s: Float3) = Mat4(Float4(x = s.x), Float4(y = s.y), Float4(z = s.z)) +fun scale(m: Mat4) = scale(m.scale) + +fun translation(t: Float3) = Mat4(w = Float4(t, 1.0f)) +fun translation(m: Mat4) = translation(m.translation) + +fun rotation(m: Mat4) = Mat4(normalize(m.right), normalize(m.up), normalize(m.forward)) +fun rotation(d: Float3): Mat4 { + val r = transform(d, ::radians) + val c = transform(r, { x -> cos(x) }) + val s = transform(r, { x -> sin(x) }) + + return Mat4.of( + c.y * c.z, -c.x * s.z + s.x * s.y * c.z, s.x * s.z + c.x * s.y * c.z, 0.0f, + c.y * s.z, c.x * c.z + s.x * s.y * s.z, -s.x * c.z + c.x * s.y * s.z, 0.0f, + -s.y , s.x * c.y , c.x * c.y , 0.0f, + 0.0f , 0.0f , 0.0f , 1.0f + ) +} +fun rotation(axis: Float3, angle: Float): Mat4 { + val x = axis.x + val y = axis.y + val z = axis.z + + val r = radians(angle) + val c = cos(r) + val s = sin(r) + val d = 1.0f - c + + return Mat4.of( + x * x * d + c , x * y * d - z * s, x * z * d + y * s, 0.0f, + y * x * d + z * s, y * y * d + c , y * z * d - x * s, 0.0f, + z * x * d - y * s, z * y * d + x * s, z * z * d + c , 0.0f, + 0.0f , 0.0f , 0.0f , 1.0f + ) +} + +fun normal(m: Mat4) = scale(1.0f / Float3(length2(m.right), length2(m.up), length2(m.forward))) * m + +fun lookAt(eye: Float3, target: Float3, up: Float3 = Float3(z = 1.0f)): Mat4 { + return lookTowards(eye, target - eye, up) +} + +fun lookTowards(eye: Float3, forward: Float3, up: Float3 = Float3(z = 1.0f)): Mat4 { + val f = normalize(forward) + val r = normalize(f x up) + val u = normalize(r x f) + return Mat4(Float4(r), Float4(u), Float4(f), Float4(eye, 1.0f)) +} + +fun perspective(fov: Float, ratio: Float, near: Float, far: Float): Mat4 { + val t = 1.0f / tan(radians(fov) * 0.5f) + val a = (far + near) / (far - near) + val b = (2.0f * far * near) / (far - near) + val c = t / ratio + return Mat4(Float4(x = c), Float4(y = t), Float4(z = a, w = 1.0f), Float4(z = -b)) +} + +fun ortho(l: Float, r: Float, b: Float, t: Float, n: Float, f: Float) = Mat4( + Float4(x = 2.0f / (r - 1.0f)), + Float4(y = 2.0f / (t - b)), + Float4(z = -2.0f / (f - n)), + Float4(-(r + l) / (r - l), -(t + b) / (t - b), -(f + n) / (f - n), 1.0f) +) + + diff --git a/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Ray.kt b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Ray.kt new file mode 100644 index 0000000..573b02f --- /dev/null +++ b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Ray.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.curiouscreature.kotlin.math + +data class Ray(var origin: Float3 = Float3(), var direction: Float3) + +fun pointAt(r: Ray, t: Float) = r.origin + r.direction * t diff --git a/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Scalar.kt b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Scalar.kt new file mode 100644 index 0000000..233a16a --- /dev/null +++ b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Scalar.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package com.curiouscreature.kotlin.math + +const val PI = 3.1415926536f +const val HALF_PI = PI * 0.5f +const val TWO_PI = PI * 2.0f +const val FOUR_PI = PI * 4.0f +const val INV_PI = 1.0f / PI +const val INV_TWO_PI = INV_PI * 0.5f +const val INV_FOUR_PI = INV_PI * 0.25f + +inline fun clamp(x: Float, min: Float, max: Float)= if (x < min) min else (if (x > max) max else x) + +inline fun saturate(x: Float) = clamp(x, 0.0f, 1.0f) + +inline fun mix(a: Float, b: Float, x: Float) = a * (1.0f - x) + b * x + +inline fun degrees(v: Float) = v * (180.0f * INV_PI) + +inline fun radians(v: Float) = v * (PI / 180.0f) + +inline fun fract(v: Float) = v % 1 + +inline fun sqr(v: Float) = v * v + +//inline fun pow(x: Float, y: Float) = StrictMath.pow(x.toDouble(), y.toDouble()).toFloat() diff --git a/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Vector.kt b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Vector.kt new file mode 100644 index 0000000..38a83d9 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/3dengine/src/commonMain/kotlin/com/curiouscreature/kotlin/math/Vector.kt @@ -0,0 +1,1299 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package com.curiouscreature.kotlin.math + +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt + +enum class VectorComponent { + X, Y, Z, W, + R, G, B, A, + S, T, P, Q +} + +data class Float2(var x: Float = 0.0f, var y: Float = 0.0f) { + constructor(v: Float) : this(v, v) + constructor(v: Float2) : this(v.x, v.y) + + inline var r: Float + get() = x + set(value) { + x = value + } + inline var g: Float + get() = y + set(value) { + y = value + } + + inline var s: Float + get() = x + set(value) { + x = value + } + inline var t: Float + get() = y + set(value) { + y = value + } + + inline var xy: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + else -> throw IllegalArgumentException("index must be X, Y, R, G, S or T") + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Float2 { + return Float2(get(index1), get(index2)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + else -> throw IllegalArgumentException("index must be in 0..1") + } + + operator fun get(index1: Int, index2: Int) = Float2(get(index1), get(index2)) + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Float) = when (index) { + 0 -> x = v + 1 -> y = v + else -> throw IllegalArgumentException("index must be in 0..1") + } + + operator fun set(index1: Int, index2: Int, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun set(index: VectorComponent, v: Float) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + else -> throw IllegalArgumentException("index must be X, Y, R, G, S or T") + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun unaryMinus() = Float2(-x, -y) + operator fun inc(): Float2 { + x += 1.0f + y += 1.0f + return this + } + + operator fun dec(): Float2 { + x -= 1.0f + y -= 1.0f + return this + } + + inline operator fun plus(v: Float) = Float2(x + v, y + v) + inline operator fun minus(v: Float) = Float2(x - v, y - v) + inline operator fun times(v: Float) = Float2(x * v, y * v) + inline operator fun div(v: Float) = Float2(x / v, y / v) + + inline operator fun plus(v: Float2) = Float2(x + v.x, y + v.y) + inline operator fun minus(v: Float2) = Float2(x - v.x, y - v.y) + inline operator fun times(v: Float2) = Float2(x * v.x, y * v.y) + inline operator fun div(v: Float2) = Float2(x / v.x, y / v.y) + + inline fun transform(block: (Float) -> Float): Float2 { + x = block(x) + y = block(y) + return this + } +} + +data class Float3(var x: Float = 0.0f, var y: Float = 0.0f, var z: Float = 0.0f) { + constructor(v: Float) : this(v, v, v) + constructor(v: Float2, z: Float = 0.0f) : this(v.x, v.y, z) + constructor(v: Float3) : this(v.x, v.y, v.z) + + inline var r: Float + get() = x + set(value) { + x = value + } + inline var g: Float + get() = y + set(value) { + y = value + } + inline var b: Float + get() = z + set(value) { + z = value + } + + inline var s: Float + get() = x + set(value) { + x = value + } + inline var t: Float + get() = y + set(value) { + y = value + } + inline var p: Float + get() = z + set(value) { + z = value + } + + inline var xy: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + + inline var rgb: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var xyz: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var stp: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z + else -> throw IllegalArgumentException("index must be X, Y, Z, R, G, B, S, T or P") + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Float2 { + return Float2(get(index1), get(index2)) + } + operator fun get( + index1: VectorComponent, index2: VectorComponent, index3: VectorComponent): Float3 { + return Float3(get(index1), get(index2), get(index3)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + 2 -> z + else -> throw IllegalArgumentException("index must be in 0..2") + } + + operator fun get(index1: Int, index2: Int) = Float2(get(index1), get(index2)) + operator fun get(index1: Int, index2: Int, index3: Int): Float3 { + return Float3(get(index1), get(index2), get(index3)) + } + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Float) = when (index) { + 0 -> x = v + 1 -> y = v + 2 -> z = v + else -> throw IllegalArgumentException("index must be in 0..2") + } + + operator fun set(index1: Int, index2: Int, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index: VectorComponent, v: Float) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z = v + else -> throw IllegalArgumentException("index must be X, Y, Z, R, G, B, S, T or P") + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun set( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent, + v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun unaryMinus() = Float3(-x, -y, -z) + operator fun inc(): Float3 { + x += 1.0f + y += 1.0f + z += 1.0f + return this + } + + operator fun dec(): Float3 { + x -= 1.0f + y -= 1.0f + z -= 1.0f + return this + } + + inline operator fun plus(v: Float) = Float3(x + v, y + v, z + v) + inline operator fun minus(v: Float) = Float3(x - v, y - v, z - v) + inline operator fun times(v: Float) = Float3(x * v, y * v, z * v) + inline operator fun div(v: Float) = Float3(x / v, y / v, z / v) + + inline operator fun plus(v: Float2) = Float3(x + v.x, y + v.y, z) + inline operator fun minus(v: Float2) = Float3(x - v.x, y - v.y, z) + inline operator fun times(v: Float2) = Float3(x * v.x, y * v.y, z) + inline operator fun div(v: Float2) = Float3(x / v.x, y / v.y, z) + + inline operator fun plus(v: Float3) = Float3(x + v.x, y + v.y, z + v.z) + inline operator fun minus(v: Float3) = Float3(x - v.x, y - v.y, z - v.z) + inline operator fun times(v: Float3) = Float3(x * v.x, y * v.y, z * v.z) + inline operator fun div(v: Float3) = Float3(x / v.x, y / v.y, z / v.z) + + inline fun transform(block: (Float) -> Float): Float3 { + x = block(x) + y = block(y) + z = block(z) + return this + } +} + +data class Float4( + var x: Float = 0.0f, + var y: Float = 0.0f, + var z: Float = 0.0f, + var w: Float = 0.0f) { + constructor(v: Float) : this(v, v, v, v) + constructor(v: Float2, z: Float = 0.0f, w: Float = 0.0f) : this(v.x, v.y, z, w) + constructor(v: Float3, w: Float = 0.0f) : this(v.x, v.y, v.z, w) + constructor(v: Float4) : this(v.x, v.y, v.z, v.w) + + inline var r: Float + get() = x + set(value) { + x = value + } + inline var g: Float + get() = y + set(value) { + y = value + } + inline var b: Float + get() = z + set(value) { + z = value + } + inline var a: Float + get() = w + set(value) { + w = value + } + + inline var s: Float + get() = x + set(value) { + x = value + } + inline var t: Float + get() = y + set(value) { + y = value + } + inline var p: Float + get() = z + set(value) { + z = value + } + inline var q: Float + get() = w + set(value) { + w = value + } + + inline var xy: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Float2 + get() = Float2(x, y) + set(value) { + x = value.x + y = value.y + } + + inline var rgb: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var xyz: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var stp: Float3 + get() = Float3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + + inline var rgba: Float4 + get() = Float4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + inline var xyzw: Float4 + get() = Float4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + inline var stpq: Float4 + get() = Float4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z + VectorComponent.W, VectorComponent.A, VectorComponent.Q -> w + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Float2 { + return Float2(get(index1), get(index2)) + } + operator fun get( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent): Float3 { + return Float3(get(index1), get(index2), get(index3)) + } + operator fun get( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent, + index4: VectorComponent): Float4 { + return Float4(get(index1), get(index2), get(index3), get(index4)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> throw IllegalArgumentException("index must be in 0..3") + } + + operator fun get(index1: Int, index2: Int) = Float2(get(index1), get(index2)) + operator fun get(index1: Int, index2: Int, index3: Int): Float3 { + return Float3(get(index1), get(index2), get(index3)) + } + operator fun get(index1: Int, index2: Int, index3: Int, index4: Int): Float4 { + return Float4(get(index1), get(index2), get(index3), get(index4)) + } + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Float) = when (index) { + 0 -> x = v + 1 -> y = v + 2 -> z = v + 3 -> w = v + else -> throw IllegalArgumentException("index must be in 0..3") + } + + operator fun set(index1: Int, index2: Int, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, index4: Int, v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + set(index4, v) + } + + operator fun set(index: VectorComponent, v: Float) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z = v + VectorComponent.W, VectorComponent.A, VectorComponent.Q -> w = v + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Float) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, index3: VectorComponent, + v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, + index3: VectorComponent, index4: VectorComponent, v: Float) { + set(index1, v) + set(index2, v) + set(index3, v) + set(index4, v) + } + + operator fun unaryMinus() = Float4(-x, -y, -z, -w) + operator fun inc(): Float4 { + x += 1.0f + y += 1.0f + z += 1.0f + w += 1.0f + return this + } + + operator fun dec(): Float4 { + x -= 1.0f + y -= 1.0f + z -= 1.0f + w -= 1.0f + return this + } + + inline operator fun plus(v: Float) = Float4(x + v, y + v, z + v, w + v) + inline operator fun minus(v: Float) = Float4(x - v, y - v, z - v, w - v) + inline operator fun times(v: Float) = Float4(x * v, y * v, z * v, w * v) + inline operator fun div(v: Float) = Float4(x / v, y / v, z / v, w / v) + + inline operator fun plus(v: Float2) = Float4(x + v.x, y + v.y, z, w) + inline operator fun minus(v: Float2) = Float4(x - v.x, y - v.y, z, w) + inline operator fun times(v: Float2) = Float4(x * v.x, y * v.y, z, w) + inline operator fun div(v: Float2) = Float4(x / v.x, y / v.y, z, w) + + inline operator fun plus(v: Float3) = Float4(x + v.x, y + v.y, z + v.z, w) + inline operator fun minus(v: Float3) = Float4(x - v.x, y - v.y, z - v.z, w) + inline operator fun times(v: Float3) = Float4(x * v.x, y * v.y, z * v.z, w) + inline operator fun div(v: Float3) = Float4(x / v.x, y / v.y, z / v.z, w) + + inline operator fun plus(v: Float4) = Float4(x + v.x, y + v.y, z + v.z, w + v.w) + inline operator fun minus(v: Float4) = Float4(x - v.x, y - v.y, z - v.z, w - v.w) + inline operator fun times(v: Float4) = Float4(x * v.x, y * v.y, z * v.z, w * v.w) + inline operator fun div(v: Float4) = Float4(x / v.x, y / v.y, z / v.z, w / v.w) + + inline fun transform(block: (Float) -> Float): Float4 { + x = block(x) + y = block(y) + z = block(z) + w = block(w) + return this + } +} + +inline operator fun Float.plus(v: Float2) = Float2(this + v.x, this + v.y) +inline operator fun Float.minus(v: Float2) = Float2(this - v.x, this - v.y) +inline operator fun Float.times(v: Float2) = Float2(this * v.x, this * v.y) +inline operator fun Float.div(v: Float2) = Float2(this / v.x, this / v.y) + +inline fun abs(v: Float2) = Float2(abs(v.x), abs(v.y)) +inline fun length(v: Float2) = sqrt(v.x * v.x + v.y * v.y) +inline fun length2(v: Float2) = v.x * v.x + v.y * v.y +inline fun distance(a: Float2, b: Float2) = length(a - b) +inline fun dot(a: Float2, b: Float2) = a.x * b.x + a.y * b.y +fun normalize(v: Float2): Float2 { + val l = 1.0f / length(v) + return Float2(v.x * l, v.y * l) +} + +inline fun reflect(i: Float2, n: Float2) = i - 2.0f * dot(n, i) * n +fun refract(i: Float2, n: Float2, eta: Float): Float2 { + val d = dot(n, i) + val k = 1.0f - eta * eta * (1.0f - sqr(d)) + return if (k < 0.0f) Float2(0.0f) else eta * i - (eta * d + sqrt(k)) * n +} + +inline fun clamp(v: Float2, min: Float, max: Float): Float2 { + return Float2( + clamp(v.x, min, max), + clamp(v.y, min, max)) +} + +inline fun clamp(v: Float2, min: Float2, max: Float2): Float2 { + return Float2( + clamp(v.x, min.x, max.x), + clamp(v.y, min.y, max.y)) +} + +inline fun mix(a: Float2, b: Float2, x: Float): Float2 { + return Float2( + mix(a.x, b.x, x), + mix(a.y, b.y, x)) +} + +inline fun mix(a: Float2, b: Float2, x: Float2): Float2 { + return Float2( + mix(a.x, b.x, x.x), + mix(a.y, b.y, x.y)) +} + +inline fun min(v: Float2) = min(v.x, v.y) +inline fun min(a: Float2, b: Float2) = Float2(min(a.x, b.x), min(a.y, b.y)) +inline fun max(v: Float2) = max(v.x, v.y) +inline fun max(a: Float2, b: Float2) = Float2(max(a.x, b.x), max(a.y, b.y)) + +inline fun transform(v: Float2, block: (Float) -> Float) = v.copy().transform(block) + +inline fun lessThan(a: Float2, b: Float) = Bool2(a.x < b, a.y < b) +inline fun lessThan(a: Float2, b: Float2) = Bool2(a.x < b.x, a.y < b.y) +inline fun lessThanEqual(a: Float2, b: Float) = Bool2(a.x <= b, a.y <= b) +inline fun lessThanEqual(a: Float2, b: Float2) = Bool2(a.x <= b.x, a.y <= b.y) +inline fun greaterThan(a: Float2, b: Float) = Bool2(a.x > b, a.y > b) +inline fun greaterThan(a: Float2, b: Float2) = Bool2(a.x > b.y, a.y > b.y) +inline fun greaterThanEqual(a: Float2, b: Float) = Bool2(a.x >= b, a.y >= b) +inline fun greaterThanEqual(a: Float2, b: Float2) = Bool2(a.x >= b.x, a.y >= b.y) +inline fun equal(a: Float2, b: Float) = Bool2(a.x == b, a.y == b) +inline fun equal(a: Float2, b: Float2) = Bool2(a.x == b.x, a.y == b.y) +inline fun notEqual(a: Float2, b: Float) = Bool2(a.x != b, a.y != b) +inline fun notEqual(a: Float2, b: Float2) = Bool2(a.x != b.x, a.y != b.y) + +inline infix fun Float2.lt(b: Float) = Bool2(x < b, y < b) +inline infix fun Float2.lt(b: Float2) = Bool2(x < b.x, y < b.y) +inline infix fun Float2.lte(b: Float) = Bool2(x <= b, y <= b) +inline infix fun Float2.lte(b: Float2) = Bool2(x <= b.x, y <= b.y) +inline infix fun Float2.gt(b: Float) = Bool2(x > b, y > b) +inline infix fun Float2.gt(b: Float2) = Bool2(x > b.x, y > b.y) +inline infix fun Float2.gte(b: Float) = Bool2(x >= b, y >= b) +inline infix fun Float2.gte(b: Float2) = Bool2(x >= b.x, y >= b.y) +inline infix fun Float2.eq(b: Float) = Bool2(x == b, y == b) +inline infix fun Float2.eq(b: Float2) = Bool2(x == b.x, y == b.y) +inline infix fun Float2.neq(b: Float) = Bool2(x != b, y != b) +inline infix fun Float2.neq(b: Float2) = Bool2(x != b.x, y != b.y) + +inline fun any(v: Bool2) = v.x || v.y +inline fun all(v: Bool2) = v.x && v.y + +inline operator fun Float.plus(v: Float3) = Float3(this + v.x, this + v.y, this + v.z) +inline operator fun Float.minus(v: Float3) = Float3(this - v.x, this - v.y, this - v.z) +inline operator fun Float.times(v: Float3) = Float3(this * v.x, this * v.y, this * v.z) +inline operator fun Float.div(v: Float3) = Float3(this / v.x, this / v.y, this / v.z) + +inline fun abs(v: Float3) = Float3(abs(v.x), abs(v.y), abs(v.z)) +inline fun length(v: Float3) = sqrt(v.x * v.x + v.y * v.y + v.z * v.z) +inline fun length2(v: Float3) = v.x * v.x + v.y * v.y + v.z * v.z +inline fun distance(a: Float3, b: Float3) = length(a - b) +inline fun dot(a: Float3, b: Float3) = a.x * b.x + a.y * b.y + a.z * b.z +inline fun cross(a: Float3, b: Float3): Float3 { + return Float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x) +} +inline infix fun Float3.x(v: Float3): Float3 { + return Float3(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x) +} +fun normalize(v: Float3): Float3 { + val l = 1.0f / length(v) + return Float3(v.x * l, v.y * l, v.z * l) +} + +inline fun reflect(i: Float3, n: Float3) = i - 2.0f * dot(n, i) * n +fun refract(i: Float3, n: Float3, eta: Float): Float3 { + val d = dot(n, i) + val k = 1.0f - eta * eta * (1.0f - sqr(d)) + return if (k < 0.0f) Float3(0.0f) else eta * i - (eta * d + sqrt(k)) * n +} + +inline fun clamp(v: Float3, min: Float, max: Float): Float3 { + return Float3( + clamp(v.x, min, max), + clamp(v.y, min, max), + clamp(v.z, min, max)) +} + +inline fun clamp(v: Float3, min: Float3, max: Float3): Float3 { + return Float3( + clamp(v.x, min.x, max.x), + clamp(v.y, min.y, max.y), + clamp(v.z, min.z, max.z)) +} + +inline fun mix(a: Float3, b: Float3, x: Float): Float3 { + return Float3( + mix(a.x, b.x, x), + mix(a.y, b.y, x), + mix(a.z, b.z, x)) +} + +inline fun mix(a: Float3, b: Float3, x: Float3): Float3 { + return Float3( + mix(a.x, b.x, x.x), + mix(a.y, b.y, x.y), + mix(a.z, b.z, x.z)) +} + +inline fun min(v: Float3) = min(v.x, min(v.y, v.z)) +inline fun min(a: Float3, b: Float3) = Float3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)) +inline fun max(v: Float3) = max(v.x, max(v.y, v.z)) +inline fun max(a: Float3, b: Float3) = Float3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)) + +inline fun transform(v: Float3, block: (Float) -> Float) = v.copy().transform(block) + +inline fun lessThan(a: Float3, b: Float) = Bool3(a.x < b, a.y < b, a.z < b) +inline fun lessThan(a: Float3, b: Float3) = Bool3(a.x < b.x, a.y < b.y, a.z < b.z) +inline fun lessThanEqual(a: Float3, b: Float) = Bool3(a.x <= b, a.y <= b, a.z <= b) +inline fun lessThanEqual(a: Float3, b: Float3) = Bool3(a.x <= b.x, a.y <= b.y, a.z <= b.z) +inline fun greaterThan(a: Float3, b: Float) = Bool3(a.x > b, a.y > b, a.z > b) +inline fun greaterThan(a: Float3, b: Float3) = Bool3(a.x > b.y, a.y > b.y, a.z > b.z) +inline fun greaterThanEqual(a: Float3, b: Float) = Bool3(a.x >= b, a.y >= b, a.z >= b) +inline fun greaterThanEqual(a: Float3, b: Float3) = Bool3(a.x >= b.x, a.y >= b.y, a.z >= b.z) +inline fun equal(a: Float3, b: Float) = Bool3(a.x == b, a.y == b, a.z == b) +inline fun equal(a: Float3, b: Float3) = Bool3(a.x == b.x, a.y == b.y, a.z == b.z) +inline fun notEqual(a: Float3, b: Float) = Bool3(a.x != b, a.y != b, a.z != b) +inline fun notEqual(a: Float3, b: Float3) = Bool3(a.x != b.x, a.y != b.y, a.z != b.z) + +inline infix fun Float3.lt(b: Float) = Bool3(x < b, y < b, z < b) +inline infix fun Float3.lt(b: Float3) = Bool3(x < b.x, y < b.y, z < b.z) +inline infix fun Float3.lte(b: Float) = Bool3(x <= b, y <= b, z <= b) +inline infix fun Float3.lte(b: Float3) = Bool3(x <= b.x, y <= b.y, z <= b.z) +inline infix fun Float3.gt(b: Float) = Bool3(x > b, y > b, z > b) +inline infix fun Float3.gt(b: Float3) = Bool3(x > b.x, y > b.y, z > b.z) +inline infix fun Float3.gte(b: Float) = Bool3(x >= b, y >= b, z >= b) +inline infix fun Float3.gte(b: Float3) = Bool3(x >= b.x, y >= b.y, z >= b.z) +inline infix fun Float3.eq(b: Float) = Bool3(x == b, y == b, z == b) +inline infix fun Float3.eq(b: Float3) = Bool3(x == b.x, y == b.y, z == b.z) +inline infix fun Float3.neq(b: Float) = Bool3(x != b, y != b, z != b) +inline infix fun Float3.neq(b: Float3) = Bool3(x != b.x, y != b.y, z != b.z) + +inline fun any(v: Bool3) = v.x || v.y || v.z +inline fun all(v: Bool3) = v.x && v.y && v.z + +inline operator fun Float.plus(v: Float4) = Float4(this + v.x, this + v.y, this + v.z, this + v.w) +inline operator fun Float.minus(v: Float4) = Float4(this - v.x, this - v.y, this - v.z, this - v.w) +inline operator fun Float.times(v: Float4) = Float4(this * v.x, this * v.y, this * v.z, this * v.w) +inline operator fun Float.div(v: Float4) = Float4(this / v.x, this / v.y, this / v.z, this / v.w) + +inline fun abs(v: Float4) = Float4(abs(v.x), abs(v.y), abs(v.z), abs(v.w)) +inline fun length(v: Float4) = sqrt(v.x * v.x + v.y * v.y + v.z * v.z + v.w * v.w) +inline fun length2(v: Float4) = v.x * v.x + v.y * v.y + v.z * v.z + v.w * v.w +inline fun distance(a: Float4, b: Float4) = length(a - b) +inline fun dot(a: Float4, b: Float4) = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w +fun normalize(v: Float4): Float4 { + val l = 1.0f / length(v) + return Float4(v.x * l, v.y * l, v.z * l, v.w * l) +} + +inline fun clamp(v: Float4, min: Float, max: Float): Float4 { + return Float4( + clamp(v.x, min, max), + clamp(v.y, min, max), + clamp(v.z, min, max), + clamp(v.w, min, max)) +} + +inline fun clamp(v: Float4, min: Float4, max: Float4): Float4 { + return Float4( + clamp(v.x, min.x, max.x), + clamp(v.y, min.y, max.y), + clamp(v.z, min.z, max.z), + clamp(v.w, min.z, max.w)) +} + +inline fun mix(a: Float4, b: Float4, x: Float): Float4 { + return Float4( + mix(a.x, b.x, x), + mix(a.y, b.y, x), + mix(a.z, b.z, x), + mix(a.w, b.w, x)) +} + +inline fun mix(a: Float4, b: Float4, x: Float4): Float4 { + return Float4( + mix(a.x, b.x, x.x), + mix(a.y, b.y, x.y), + mix(a.z, b.z, x.z), + mix(a.w, b.w, x.w)) +} + +inline fun min(v: Float4) = min(v.x, min(v.y, min(v.z, v.w))) +inline fun min(a: Float4, b: Float4): Float4 { + return Float4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w)) +} +inline fun max(v: Float4) = max(v.x, max(v.y, max(v.z, v.w))) +inline fun max(a: Float4, b: Float4): Float4 { + return Float4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w)) +} + +inline fun transform(v: Float4, block: (Float) -> Float) = v.copy().transform(block) + +inline fun lessThan(a: Float4, b: Float) = Bool4(a.x < b, a.y < b, a.z < b, a.w < b) +inline fun lessThan(a: Float4, b: Float4) = Bool4(a.x < b.x, a.y < b.y, a.z < b.z, a.w < b.w) +inline fun lessThanEqual(a: Float4, b: Float) = Bool4(a.x <= b, a.y <= b, a.z <= b, a.w <= b) +inline fun lessThanEqual(a: Float4, b: Float4) = Bool4(a.x <= b.x, a.y <= b.y, a.z <= b.z, a.w <= b.w) +inline fun greaterThan(a: Float4, b: Float) = Bool4(a.x > b, a.y > b, a.z > b, a.w > b) +inline fun greaterThan(a: Float4, b: Float4) = Bool4(a.x > b.y, a.y > b.y, a.z > b.z, a.w > b.w) +inline fun greaterThanEqual(a: Float4, b: Float) = Bool4(a.x >= b, a.y >= b, a.z >= b, a.w >= b) +inline fun greaterThanEqual(a: Float4, b: Float4) = Bool4(a.x >= b.x, a.y >= b.y, a.z >= b.z, a.w >= b.w) +inline fun equal(a: Float4, b: Float) = Bool4(a.x == b, a.y == b, a.z == b, a.w == b) +inline fun equal(a: Float4, b: Float4) = Bool4(a.x == b.x, a.y == b.y, a.z == b.z, a.w == b.w) +inline fun notEqual(a: Float4, b: Float) = Bool4(a.x != b, a.y != b, a.z != b, a.w != b) +inline fun notEqual(a: Float4, b: Float4) = Bool4(a.x != b.x, a.y != b.y, a.z != b.z, a.w != b.w) + +inline infix fun Float4.lt(b: Float) = Bool4(x < b, y < b, z < b, a < b) +inline infix fun Float4.lt(b: Float4) = Bool4(x < b.x, y < b.y, z < b.z, w < b.w) +inline infix fun Float4.lte(b: Float) = Bool4(x <= b, y <= b, z <= b, w <= b) +inline infix fun Float4.lte(b: Float4) = Bool4(x <= b.x, y <= b.y, z <= b.z, w <= b.w) +inline infix fun Float4.gt(b: Float) = Bool4(x > b, y > b, z > b, w > b) +inline infix fun Float4.gt(b: Float4) = Bool4(x > b.x, y > b.y, z > b.z, w > b.w) +inline infix fun Float4.gte(b: Float) = Bool4(x >= b, y >= b, z >= b, w >= b) +inline infix fun Float4.gte(b: Float4) = Bool4(x >= b.x, y >= b.y, z >= b.z, w >= b.w) +inline infix fun Float4.eq(b: Float) = Bool4(x == b, y == b, z == b, w == b) +inline infix fun Float4.eq(b: Float4) = Bool4(x == b.x, y == b.y, z == b.z, w == b.w) +inline infix fun Float4.neq(b: Float) = Bool4(x != b, y != b, z != b, w != b) +inline infix fun Float4.neq(b: Float4) = Bool4(x != b.x, y != b.y, z != b.z, w != b.w) + +inline fun any(v: Bool4) = v.x || v.y || v.z || v.w +inline fun all(v: Bool4) = v.x && v.y && v.z && v.w + +data class Bool2(var x: Boolean = false, var y: Boolean = false) { + constructor(v: Bool2) : this(v.x, v.y) + + inline var r: Boolean + get() = x + set(value) { + x = value + } + inline var g: Boolean + get() = y + set(value) { + y = value + } + + inline var s: Boolean + get() = x + set(value) { + x = value + } + inline var t: Boolean + get() = y + set(value) { + y = value + } + + inline var xy: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + else -> throw IllegalArgumentException("index must be X, Y, R, G, S or T") + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Bool2 { + return Bool2(get(index1), get(index2)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + else -> throw IllegalArgumentException("index must be in 0..1") + } + + operator fun get(index1: Int, index2: Int) = Bool2(get(index1), get(index2)) + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Boolean) = when (index) { + 0 -> x = v + 1 -> y = v + else -> throw IllegalArgumentException("index must be in 0..1") + } + + operator fun set(index1: Int, index2: Int, v: Boolean) { + set(index1, v) + set(index2, v) + } + + operator fun set(index: VectorComponent, v: Boolean) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + else -> throw IllegalArgumentException("index must be X, Y, R, G, S or T") + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Boolean) { + set(index1, v) + set(index2, v) + } +} + +data class Bool3(var x: Boolean = false, var y: Boolean = false, var z: Boolean = false) { + constructor(v: Bool2, z: Boolean = false) : this(v.x, v.y, z) + constructor(v: Bool3) : this(v.x, v.y, v.z) + + inline var r: Boolean + get() = x + set(value) { + x = value + } + inline var g: Boolean + get() = y + set(value) { + y = value + } + inline var b: Boolean + get() = z + set(value) { + z = value + } + + inline var s: Boolean + get() = x + set(value) { + x = value + } + inline var t: Boolean + get() = y + set(value) { + y = value + } + inline var p: Boolean + get() = z + set(value) { + z = value + } + + inline var xy: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + + inline var rgb: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var xyz: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var stp: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z + else -> throw IllegalArgumentException("index must be X, Y, Z, R, G, B, S, T or P") + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Bool2 { + return Bool2(get(index1), get(index2)) + } + operator fun get( + index1: VectorComponent, index2: VectorComponent, index3: VectorComponent): Bool3 { + return Bool3(get(index1), get(index2), get(index3)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + 2 -> z + else -> throw IllegalArgumentException("index must be in 0..2") + } + + operator fun get(index1: Int, index2: Int) = Bool2(get(index1), get(index2)) + operator fun get(index1: Int, index2: Int, index3: Int): Bool3 { + return Bool3(get(index1), get(index2), get(index3)) + } + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Boolean) = when (index) { + 0 -> x = v + 1 -> y = v + 2 -> z = v + else -> throw IllegalArgumentException("index must be in 0..2") + } + + operator fun set(index1: Int, index2: Int, v: Boolean) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index: VectorComponent, v: Boolean) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z = v + else -> throw IllegalArgumentException("index must be X, Y, Z, R, G, B, S, T or P") + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Boolean) { + set(index1, v) + set(index2, v) + } + + operator fun set( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent, + v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + } +} + +data class Bool4( + var x: Boolean = false, + var y: Boolean = false, + var z: Boolean = false, + var w: Boolean = false) { + constructor(v: Bool2, z: Boolean = false, w: Boolean = false) : this(v.x, v.y, z, w) + constructor(v: Bool3, w: Boolean = false) : this(v.x, v.y, v.z, w) + constructor(v: Bool4) : this(v.x, v.y, v.z, v.w) + + inline var r: Boolean + get() = x + set(value) { + x = value + } + inline var g: Boolean + get() = y + set(value) { + y = value + } + inline var b: Boolean + get() = z + set(value) { + z = value + } + inline var a: Boolean + get() = w + set(value) { + w = value + } + + inline var s: Boolean + get() = x + set(value) { + x = value + } + inline var t: Boolean + get() = y + set(value) { + y = value + } + inline var p: Boolean + get() = z + set(value) { + z = value + } + inline var q: Boolean + get() = w + set(value) { + w = value + } + + inline var xy: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var rg: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + inline var st: Bool2 + get() = Bool2(x, y) + set(value) { + x = value.x + y = value.y + } + + inline var rgb: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var xyz: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + inline var stp: Bool3 + get() = Bool3(x, y, z) + set(value) { + x = value.x + y = value.y + z = value.z + } + + inline var rgba: Bool4 + get() = Bool4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + inline var xyzw: Bool4 + get() = Bool4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + inline var stpq: Bool4 + get() = Bool4(x, y, z, w) + set(value) { + x = value.x + y = value.y + z = value.z + w = value.w + } + + operator fun get(index: VectorComponent) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z + VectorComponent.W, VectorComponent.A, VectorComponent.Q -> w + } + + operator fun get(index1: VectorComponent, index2: VectorComponent): Bool2 { + return Bool2(get(index1), get(index2)) + } + operator fun get( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent): Bool3 { + return Bool3(get(index1), get(index2), get(index3)) + } + operator fun get( + index1: VectorComponent, + index2: VectorComponent, + index3: VectorComponent, + index4: VectorComponent): Bool4 { + return Bool4(get(index1), get(index2), get(index3), get(index4)) + } + + operator fun get(index: Int) = when (index) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> throw IllegalArgumentException("index must be in 0..3") + } + + operator fun get(index1: Int, index2: Int) = Bool2(get(index1), get(index2)) + operator fun get(index1: Int, index2: Int, index3: Int): Bool3 { + return Bool3(get(index1), get(index2), get(index3)) + } + operator fun get(index1: Int, index2: Int, index3: Int, index4: Int): Bool4 { + return Bool4(get(index1), get(index2), get(index3), get(index4)) + } + + inline operator fun invoke(index: Int) = get(index - 1) + + operator fun set(index: Int, v: Boolean) = when (index) { + 0 -> x = v + 1 -> y = v + 2 -> z = v + 3 -> w = v + else -> throw IllegalArgumentException("index must be in 0..3") + } + + operator fun set(index1: Int, index2: Int, v: Boolean) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index1: Int, index2: Int, index3: Int, index4: Int, v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + set(index4, v) + } + + operator fun set(index: VectorComponent, v: Boolean) = when (index) { + VectorComponent.X, VectorComponent.R, VectorComponent.S -> x = v + VectorComponent.Y, VectorComponent.G, VectorComponent.T -> y = v + VectorComponent.Z, VectorComponent.B, VectorComponent.P -> z = v + VectorComponent.W, VectorComponent.A, VectorComponent.Q -> w = v + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, v: Boolean) { + set(index1, v) + set(index2, v) + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, index3: VectorComponent, + v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + } + + operator fun set(index1: VectorComponent, index2: VectorComponent, + index3: VectorComponent, index4: VectorComponent, v: Boolean) { + set(index1, v) + set(index2, v) + set(index3, v) + set(index4, v) + } +} diff --git a/app/src/main/java/com/helible/pilot/MainActivity.kt b/app/src/main/java/com/helible/pilot/MainActivity.kt index a447e98..4eb72d2 100644 --- a/app/src/main/java/com/helible/pilot/MainActivity.kt +++ b/app/src/main/java/com/helible/pilot/MainActivity.kt @@ -21,7 +21,7 @@ import com.helible.pilot.components.RotorsTestPage import com.helible.pilot.components.console.ConsolePage import com.helible.pilot.components.deviceScreen.DeviceControlScreen import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList -import com.helible.pilot.components.pidSettings.PidSettingsPage +import com.helible.pilot.components.PidSettingsPage import com.helible.pilot.components.scannerScreen.ScannerScreen import com.helible.pilot.permissions.PermissionsLauncher import com.helible.pilot.permissions.PermissionsRequest @@ -143,11 +143,38 @@ class MainActivity : ComponentActivity() { if (preferencesViewModel.preferences != null) BackHandler {} } composable("console/{title}") - { backStackEntry -> + { _ -> ConsolePage( - title = backStackEntry.arguments?.getString("title") ?: "null", - navigateBack = { navController.popBackStack() } + startTakeoff = { bluetoothViewModel.startTakeoff() }, + startOnboarding = { bluetoothViewModel.startOnboarding() }, + stop = { bluetoothViewModel.stopRotors() }, + changeStick1Position = { + _: Int, heightVelocity: Int -> + bluetoothViewModel.changeHeightStickPosition(heightVelocity) + }, + changeStick2Position = { + x: Int, y: Int -> + bluetoothViewModel.changePitchStickPosition(y) + bluetoothViewModel.changeYawStickPosition(x) + }, + bluetoothUiState = bluetoothState, + reconnect = { + val preferences = preferences.getPreferences() + if(preferences != null) { + bluetoothViewModel.connectToDevice( + preferences.deviceAddress + ) + } else { + navController.navigate("scanner") + } + }, + navigateBack = { + navController.popBackStack() + } ) + BackHandler { + navController.popBackStack() + } } composable("codeblocks/{title}") { backStackEntry -> diff --git a/app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt b/app/src/main/java/com/helible/pilot/components/PidSettingsPage.kt similarity index 99% rename from app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt rename to app/src/main/java/com/helible/pilot/components/PidSettingsPage.kt index 11316ea..5298fdd 100644 --- a/app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt +++ b/app/src/main/java/com/helible/pilot/components/PidSettingsPage.kt @@ -1,4 +1,4 @@ -package com.helible.pilot.components.pidSettings +package com.helible.pilot.components import android.util.Log import androidx.compose.foundation.clickable @@ -39,7 +39,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import com.helible.pilot.R -import com.helible.pilot.components.BlankPage import com.helible.pilot.dataclasses.DeviceState import com.helible.pilot.dataclasses.DeviceStatus import com.helible.pilot.dataclasses.PidParams diff --git a/app/src/main/java/com/helible/pilot/components/World.kt b/app/src/main/java/com/helible/pilot/components/World.kt new file mode 100644 index 0000000..4609bd8 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/components/World.kt @@ -0,0 +1,63 @@ +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.curiouscreature.kotlin.math.Float3 + + +@Composable +fun World(mesh: Mesh = cube, drawVertices: MutableState, camerax: MutableState) { + val camera = Camera( + position = Float3(20f, 110.1f, 110.0f), + target = Float3(0.1f, camerax.value, 1.0f) + ) + Text("Camera: ${camera.target.y}") + val animatedProgress by rememberInfiniteTransition().animateFloat( + initialValue = 0.01f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 10000, easing = LinearEasing), + ), + ) + + Canvas(Modifier.size(50.dp, 50.dp)) { + mesh.rotation = Float3(animatedProgress + 90, animatedProgress + 180, 0.01f) + camera.target.y += 0.1f + camera.target.x += 0.1f + camera.target.z -= 0.1f + val lines = render3d(camera, mesh) + lines.forEach { (one, two, three) -> + if (drawVertices.value) { + drawCircle(color = Color.Cyan, radius = 5f, center = Offset(one.x, one.y)) + drawCircle(color = Color.Cyan, radius = 5f, center = Offset(two.x, two.y)) + drawCircle(color = Color.Cyan, radius = 5f, center = Offset(three.x, three.y)) + } + drawLine( + color = Color.Red, + start = Offset(one.x, one.y), + end = Offset(two.x, two.y) + ) + drawLine( + color = Color.Red, + start = Offset(two.x, two.y), + end = Offset(three.x, three.y) + ) + drawLine( + color = Color.Red, + start = Offset(three.x, three.y), + end = Offset(one.x, one.y) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt b/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt index d296409..6c35c67 100644 --- a/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt +++ b/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt @@ -1,33 +1,238 @@ package com.helible.pilot.components.console +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo import android.util.Log +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import com.helible.pilot.components.BlankPage +import com.helible.pilot.R +import com.helible.pilot.dataclasses.BluetoothUiState import com.helible.pilot.dataclasses.DeviceStatus import com.manalkaff.jetstick.JoyStick @Composable fun ConsolePage( - title: String, - navigateBack: () -> Unit + startTakeoff: () -> Unit, + startOnboarding: () -> Unit, + stop: () -> Unit, + reconnect: () -> Unit, + changeStick1Position: (x: Int, y: Int) -> Unit, + changeStick2Position: (x: Int, y: Int) -> Unit, + bluetoothUiState: BluetoothUiState, + navigateBack: () -> Unit, ) { - BlankPage(title = title, navigateBack = navigateBack) { - Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + val deviceState = bluetoothUiState.deviceState + LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + LaunchedEffect(key1 = null) { + Log.i("Console", "state: ${bluetoothUiState.deviceState}, isConnected: ${bluetoothUiState.isConnected}") + } + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + if (bluetoothUiState.isConnected && deviceState != null) { + Text("Высота полёта: ${deviceState.flightHeight / 100} м; ") + Text("Рыскание: ${deviceState.yaw}°; ") + Text("Тангаж: ${deviceState.pitch}°; ") + Text("Крен: ${deviceState.roll}°; ") + } + } + Row( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + JoyStick( - Modifier.padding(30.dp), - size = 150.dp, - dotSize = 30.dp - ){ x: Float, y: Float -> + size = 200.dp, + dotSize = 80.dp, + backgroundImage = R.drawable.stick_background, + dotImage = R.drawable.stick_dot, + modifier = Modifier.padding(30.dp) + ) { x: Float, y: Float -> + changeStick1Position(x.toInt(), y.toInt()) Log.d("JoyStick", "$x, $y") } + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (bluetoothUiState.isConnected && deviceState != null) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (deviceState.status == DeviceStatus.IsPreparingForTakeoff) { + CircularProgressIndicator(modifier = Modifier.padding(7.dp)) + } + Text(text = describeStatus(deviceState.status)) + } + Text(text = "Заряд батареи: ${deviceState.batteryCharge}%") + } else { + Text(text = "Нет соединения с устройством") + } + + if (bluetoothUiState.isConnecting) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator(modifier = Modifier.padding(5.dp)) + Text(text = "Подключение...") + } + } else if (bluetoothUiState.isConnected) { + Button( + onClick = startTakeoff, + enabled = deviceState?.status == DeviceStatus.Idle + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.KeyboardArrowUp, contentDescription = null) + Text(text = "Взлёт") + } + } + Button(onClick = stop, colors = ButtonDefaults.buttonColors(Color.Red)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Warning, contentDescription = null) + Text(text = "СТОП") + } + } + Button( + onClick = startOnboarding, + enabled = deviceState?.status in listOf( + DeviceStatus.IsFlying, DeviceStatus.IsPreparingForTakeoff + ) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.KeyboardArrowDown, contentDescription = null) + Text(text = "Посадка") + } + } + } else { + Button(onClick = reconnect) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Default.Refresh, contentDescription = null) + Text(text = "Попробовать ещё раз") + } + } + } + + if(!bluetoothUiState.isEnabled || deviceState?.status in listOf(DeviceStatus.ChargeRequired, DeviceStatus.Idle, null)) { + Button( + onClick = navigateBack + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Default.ExitToApp, contentDescription = null) + Text(text = "Выход") + } + } + } + } + + JoyStick( + size = 200.dp, + dotSize = 80.dp, + backgroundImage = R.drawable.stick_background, + dotImage = R.drawable.stick_dot, + ) { x: Float, y: Float -> + changeStick2Position(x.toInt(), y.toInt()) + Log.d("JoyStick", "$x, $y") + } } } } + +@Composable +fun LockScreenOrientation(orientation: Int) { + val context = LocalContext.current + DisposableEffect(orientation) { + val activity = context.findActivity() ?: return@DisposableEffect onDispose {} + val originalOrientation = activity.requestedOrientation + activity.requestedOrientation = orientation + onDispose { + // restore original orientation when view disappears + activity.requestedOrientation = originalOrientation + } + } +} + +fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} + +fun describeStatus(deviceState: DeviceStatus): String { + return when (deviceState) { + DeviceStatus.Idle -> { + "Устройство заряжено, бездействует" + } + + DeviceStatus.IsFlying -> { + "В полёте" + } + + DeviceStatus.ChargeRequired -> { + "Устройство разряжено. Дальнейшие полёты невозможны" + } + + DeviceStatus.IsImuCalibration -> { + "Калибровка гироскопа и акселерометра. Пожалуйста, не двигайте устройство." + } + + DeviceStatus.IsPreparingForTakeoff -> { + "Подготовка ко взлёту..." + } + + DeviceStatus.IsBoarding -> { + "Посадка" + } + } +} + +@Preview(showBackground = true, device = "spec:parent=pixel_5,orientation=landscape") +@Composable +fun ConsolePreview() { + ConsolePage( + startTakeoff = {}, + startOnboarding = {}, + stop = {}, + changeStick1Position = { _: Int, _: Int -> }, + changeStick2Position = { _: Int, _: Int -> }, + bluetoothUiState = BluetoothUiState().copy( + isConnected = false, + deviceState = null + ), + reconnect = {}, + navigateBack = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/components/deviceScreen/DeviceControlScreen.kt b/app/src/main/java/com/helible/pilot/components/deviceScreen/DeviceControlScreen.kt index b2fe099..d02bcf1 100644 --- a/app/src/main/java/com/helible/pilot/components/deviceScreen/DeviceControlScreen.kt +++ b/app/src/main/java/com/helible/pilot/components/deviceScreen/DeviceControlScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.helible.pilot.R -import com.helible.pilot.components.Title +import com.helible.pilot.components.scannerScreen.Title import com.helible.pilot.dataclasses.BluetoothUiState import com.helible.pilot.viewmodels.AppPreferences diff --git a/app/src/main/java/com/helible/pilot/components/scannerScreen/ScannerScreen.kt b/app/src/main/java/com/helible/pilot/components/scannerScreen/ScannerScreen.kt index 71ea84b..a2cf3eb 100644 --- a/app/src/main/java/com/helible/pilot/components/scannerScreen/ScannerScreen.kt +++ b/app/src/main/java/com/helible/pilot/components/scannerScreen/ScannerScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import com.helible.pilot.components.Title import com.helible.pilot.dataclasses.BluetoothDevice import com.helible.pilot.dataclasses.BluetoothUiState diff --git a/app/src/main/java/com/helible/pilot/components/Title.kt b/app/src/main/java/com/helible/pilot/components/scannerScreen/Title.kt similarity index 91% rename from app/src/main/java/com/helible/pilot/components/Title.kt rename to app/src/main/java/com/helible/pilot/components/scannerScreen/Title.kt index c6c22fa..93b97e6 100644 --- a/app/src/main/java/com/helible/pilot/components/Title.kt +++ b/app/src/main/java/com/helible/pilot/components/scannerScreen/Title.kt @@ -1,4 +1,4 @@ -package com.helible.pilot.components +package com.helible.pilot.components.scannerScreen import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/helible/pilot/dataclasses/SticksPosition.kt b/app/src/main/java/com/helible/pilot/dataclasses/SticksPosition.kt new file mode 100644 index 0000000..c419a1c --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/SticksPosition.kt @@ -0,0 +1,10 @@ +package com.helible.pilot.dataclasses + +import com.squareup.moshi.Json + +data class SticksPosition( + @Json(name = "hS") val heightStick: Int, + @Json(name = "yS") val yawStick: Int, + @Json(name = "pS") val pitchStick: Int +) + diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt index 4ed829b..bf0c3ab 100644 --- a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt +++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt @@ -40,6 +40,8 @@ class BluetoothDataTransferService( } } catch (e: NoSuchElementException) { Log.e("BluetoothController", "Message type is invalid: $message") + } catch (e: NumberFormatException) { + Log.e("BluetoothController", "Message invalid, may be device buffer congested: $message") } } }.flowOn(Dispatchers.IO) diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt index a55ca09..d6d7bc1 100644 --- a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt +++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt @@ -15,6 +15,7 @@ import com.helible.pilot.dataclasses.MessageType import com.helible.pilot.dataclasses.PidSettingRequiredMessage import com.helible.pilot.dataclasses.PidSettings import com.helible.pilot.dataclasses.RotorsDuty +import com.helible.pilot.dataclasses.SticksPosition import com.helible.pilot.dataclasses.StopMessage import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonEncodingException @@ -55,11 +56,12 @@ class BluetoothViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) private val _rotorsDuty: MutableStateFlow = MutableStateFlow(RotorsDuty(0, 0, 0)) private val _isRotorsTelemetryEnabled: MutableStateFlow = MutableStateFlow(false) + private val _isConsoleTelemetryEnabled: MutableStateFlow = MutableStateFlow(false) + private val _sticksPosition: MutableStateFlow = MutableStateFlow(SticksPosition(0, 0, 0)) val rotorsDuty: StateFlow get() = _rotorsDuty.asStateFlow() - private var rotorsTelemetryJob: Job? = null private var deviceConnectionJob: Job? = null private val moshi = @@ -71,6 +73,7 @@ class BluetoothViewModel( moshi.adapter(PidSettingRequiredMessage::class.java) private val rotorDutyMessageAdapter = moshi.adapter(RotorsDuty::class.java) private val stopAllRotorsMessageAdapter = moshi.adapter(StopMessage::class.java) + private val consoleStateMessageAdapter = moshi.adapter(SticksPosition::class.java) companion object { const val messageDelimiter = "\n" @@ -145,13 +148,14 @@ class BluetoothViewModel( ) } } - } } } catch (e: JsonDataException) { Log.e("BluetoothVM", "Failed to parse message: ${result.message.data}") } catch (e: JsonEncodingException) { Log.e("BluetoothVM", "Failed to decode message: ${result.message.data}") + } catch (e: Exception) { + Log.e("BluetoothVM", "Unknown error on message: ${result.message.data}") } } @@ -168,8 +172,8 @@ class BluetoothViewModel( } .catch { throwable -> Log.e( - "BluetoothController", - "Error occured while data transfer: ${throwable.message}" + "BluetoothVM", + "Error occured while data transfer: ${throwable.localizedMessage}" ) bluetoothController.closeConnection() _state.update { @@ -196,6 +200,8 @@ class BluetoothViewModel( fun disconnectFromDevice() { deviceConnectionJob?.cancel() bluetoothController.closeConnection() + _isConsoleTelemetryEnabled.update { false } + _isRotorsTelemetryEnabled.update { false } _state.update { it.copy( isConnecting = false, @@ -314,7 +320,6 @@ class BluetoothViewModel( fun stopRotorsConfigurationTelemetry() { Log.i("BluetoothVM", "Stop send rotors configuration periodically...") - rotorsTelemetryJob = null _isRotorsTelemetryEnabled.update { false } } @@ -333,7 +338,88 @@ class BluetoothViewModel( } } else { _rotorsDuty.update { RotorsDuty(0, 0, 0) } + _isConsoleTelemetryEnabled.update { false } } } } + fun startTakeoff() { + viewModelScope.launch { + val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsFlying)) + messageDelimiter + val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) + if(!isSuccess) { + Log.e("BluetoothVM", "Failed to start takeoff: $message") + _state.update { + it.copy(errorMessage = "Не удалось начать полёт!") + } + } else { + _state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsFlying)) } + startConsoleTelemetrySending() + } + } + } + + fun startOnboarding() { + viewModelScope.launch { + val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsBoarding)) + messageDelimiter + val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) + if(!isSuccess) { + Log.e("BluetoothVM", "Failed to start onboarding: $message") + } else { + _state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsBoarding)) } + stopConsoleTelemetry() + } + } + } + + fun changeHeightStickPosition(newHeightStickPosition: Int) { + _sticksPosition.update { + it.copy(heightStick = newHeightStickPosition) + } + } + + fun changeYawStickPosition(newYawStickPosition: Int) { + _sticksPosition.update { + it.copy(yawStick = newYawStickPosition) + } + } + + fun changePitchStickPosition(newPitchStickPosition: Int) { + _sticksPosition.update { + it.copy(pitchStick = newPitchStickPosition) + } + } + + private fun sendConsoleState() { + viewModelScope.launch { + val message = consoleStateMessageAdapter.toJson( + _sticksPosition.value + ) + messageDelimiter + val isSuccess = bluetoothController.trySendMessage( + message.toByteArray() + ) + Log.i("BluetoothVM", "Sended telemetry message: $message") + if (!isSuccess) { + Log.e("BluetoothVM", "Failed to send console telemetry: $message") + } + } + } + + private fun startConsoleTelemetrySending() { + if(_isConsoleTelemetryEnabled.value) { + return + } + _isConsoleTelemetryEnabled.update { true } + flow { + while (_isConsoleTelemetryEnabled.value) { + emit(Unit) + delay(telemetryPauseDuractionMs) + } + }.onEach { + sendConsoleState() + }.launchIn(viewModelScope) + } + + private fun stopConsoleTelemetry() { + _isConsoleTelemetryEnabled.update { false } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/stick_background.xml b/app/src/main/res/drawable/stick_background.xml new file mode 100644 index 0000000..cc4a3db --- /dev/null +++ b/app/src/main/res/drawable/stick_background.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/stick_dot.xml b/app/src/main/res/drawable/stick_dot.xml new file mode 100644 index 0000000..d964489 --- /dev/null +++ b/app/src/main/res/drawable/stick_dot.xml @@ -0,0 +1,10 @@ + + +