Console screen implemented
This commit is contained in:
@@ -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<Mat3>) {
|
||||
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<Float2> {
|
||||
|
||||
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<List<Float2>> {
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
63
app/src/main/java/com/helible/pilot/components/World.kt
Normal file
63
app/src/main/java/com/helible/pilot/components/World.kt
Normal file
@@ -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<Boolean>, camerax: MutableState<Float>) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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 = {}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<RotorsDuty> = MutableStateFlow(RotorsDuty(0, 0, 0))
|
||||
private val _isRotorsTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
private val _isConsoleTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
private val _sticksPosition: MutableStateFlow<SticksPosition> = MutableStateFlow(SticksPosition(0, 0, 0))
|
||||
|
||||
val rotorsDuty: StateFlow<RotorsDuty>
|
||||
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 }
|
||||
}
|
||||
}
|
||||
10
app/src/main/res/drawable/stick_background.xml
Normal file
10
app/src/main/res/drawable/stick_background.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="210dp"
|
||||
android:height="210dp"
|
||||
android:viewportWidth="210"
|
||||
android:viewportHeight="210">
|
||||
<path
|
||||
android:pathData="M0.5,105.5a105,105 0,1 0,210 0a105,105 0,1 0,-210 0z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/stick_dot.xml
Normal file
10
app/src/main/res/drawable/stick_dot.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="95dp"
|
||||
android:height="95dp"
|
||||
android:viewportWidth="95"
|
||||
android:viewportHeight="95">
|
||||
<path
|
||||
android:pathData="M0,47.5a47.5,47.5 0,1 0,95 0a47.5,47.5 0,1 0,-95 0z"
|
||||
android:fillColor="#f83a14"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user