KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt

196 lines
5.2 KiB
Kotlin

package ru.dbotthepony.kstarbound.math
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.getAdapter
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
private operator fun Vector2d.compareTo(other: Vector2d): Int {
var cmp = x.compareTo(other.x)
if (cmp == 0) cmp = y.compareTo(y)
return cmp
}
@JsonAdapter(Line2d.Adapter::class)
data class Line2d(val p0: Vector2d, val p1: Vector2d) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d(isLegacy), stream.readVector2d(isLegacy))
val normal: Vector2d
init {
val diff = (p1 - p0).unitVector
normal = Vector2d(-diff.y, diff.x)
}
val center: Vector2d
get() = p0 + difference * 0.5
val aabb: AABB get() {
return AABB(
Vector2d(min(p0.x, p1.x), min(p0.y, p1.y)),
Vector2d(max(p0.x, p1.x), max(p0.y, p1.y)),
)
}
operator fun plus(other: IStruct2d): Line2d {
return Line2d(p0 + other, p1 + other)
}
operator fun minus(other: IStruct2d): Line2d {
return Line2d(p0 - other, p1 - other)
}
operator fun times(other: IStruct2d): Line2d {
return Line2d(p0 * other, p1 * other)
}
operator fun times(other: Double): Line2d {
return Line2d(p0 * other, p1 * other)
}
data class Intersection(val intersects: Boolean, val point: Vector2d?, val t: Double?, val coincides: Boolean, val glances: Boolean) {
companion object {
val EMPTY = Intersection(false, null, null, false, false)
}
}
val difference: Vector2d
get() = p1 - p0
fun reverse(): Line2d {
return Line2d(p1, p0)
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeStruct2d(p0, isLegacy)
stream.writeStruct2d(p1, isLegacy)
}
/**
* * if > 0.0 - left side
* * if < 0.0 - right side
* * if = 0.0 - rests upon
*/
fun isLeft(p2: IStruct2d): Double {
return (p1.component1() - p0.component1()) * (p2.component2() - p0.component2()) - (p2.component1() - p0.component1()) * (p1.component2() - p0.component2())
}
// original source of this intersection algorithm:
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
// article "Intersection of two lines in three-space" by Ronald Goldman, published in Graphics Gems, page 304
fun intersect(other: Line2d, infinite: Boolean = false): Intersection {
val (c, d) = other
val ab = difference
val cd = other.difference
val abCross = p0.cross(p1)
val cdCross = c.cross(d)
val denominator = ab.cross(cd)
val xNumber = abCross * cd.x - cdCross * ab.x
val yNumber = abCross * cd.y - cdCross * ab.y
if (denominator.absoluteValue <= NEAR_ZERO) {
if (xNumber.absoluteValue <= NEAR_ZERO && yNumber.absoluteValue <= NEAR_ZERO) {
val intersects = infinite || (p0 >= c && p0 <= d) || (c >= p0 && c <= p1)
var point: Vector2d? = null
var t = 0.0
if (intersects) {
if (infinite) {
point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)
} else {
point = if (p0 < c) c else p0
}
}
if (p0 < c) {
if (c.x != p0.x) {
t = (c.x - p0.x) / ab.x
} else {
t = (c.y - p0.y) / ab.y
}
} else if (p0 > d) {
if (d.x != p0.x) {
t = (d.x - p0.x) / ab.x
} else {
t = (d.y - p0.y) / ab.y
}
}
return Intersection(intersects, point, t, true, intersects)
} else {
return Intersection.EMPTY
}
} else {
val ta = (c - p0).cross(cd) / denominator
val tb = (c - p0).cross(ab) / denominator
val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0)
return Intersection(
intersects = intersects,
t = ta,
point = (p1 - p0) * ta + p0,
coincides = false,
glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE)
)
}
}
fun project(axis: IStruct2d): Double {
val diff = difference
val (x, y) = axis
return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared
}
fun distanceTo(other: IStruct2d, infinite: Boolean = false): Double {
var proj = project(other)
if (!infinite)
proj = proj.coerceIn(0.0, 1.0)
return (Vector2d(other) - p0 + difference * proj).length
}
fun distanceTo(other: Vector2d, infinite: Boolean = false): Double {
var proj = project(other)
if (!infinite)
proj = proj.coerceIn(0.0, 1.0)
return (other - p0 + difference * proj).length
}
class Adapter(gson: Gson) : TypeAdapter<Line2d>() {
private val pair = gson.getAdapter<Pair<Vector2d, Vector2d>>()
override fun write(out: JsonWriter, value: Line2d) {
pair.write(out, value.p0 to value.p1)
}
override fun read(`in`: JsonReader): Line2d {
val (a, b) = pair.read(`in`)
return Line2d(a, b)
}
}
companion object {
const val NEAR_ZERO = Double.MIN_VALUE * 2.0
const val NEAR_ONE = 1.0 - NEAR_ZERO
}
}