KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt
2024-04-22 16:41:05 +07:00

315 lines
8.9 KiB
Kotlin

@file:Suppress("unused", "LiftReturnOrAssignment", "MemberVisibilityCanBePrivate")
package ru.dbotthepony.kstarbound.math
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.math.intersectRectangles
import ru.dbotthepony.kommons.math.rectangleContainsRectangle
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
/**
* Axis Aligned Bounding Box, represented by two points, [mins] as lowermost corner of BB,
* and [maxs] as uppermost corner of BB
*/
data class AABB(val mins: Vector2d, val maxs: Vector2d) {
constructor(line: Line2d) : this(
Vector2d(min(line.p0.x, line.p1.x), min(line.p0.y, line.p1.y)),
Vector2d(max(line.p0.x, line.p1.x), max(line.p0.y, line.p1.y)),
)
init {
// require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
// require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" }
}
val isEmpty: Boolean
get() = mins.x > maxs.x || mins.y > maxs.y
val isZero: Boolean
get() = mins == maxs
val isEmptyOrZero: Boolean
get() = isEmpty || isZero
operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs)
operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs)
operator fun div(other: AABB) = AABB(mins / other.mins, maxs / other.maxs)
operator fun plus(other: Vector2d) = AABB(mins + other, maxs + other)
operator fun minus(other: Vector2d) = AABB(mins - other, maxs - other)
operator fun times(other: Vector2d) = AABB(mins * other, maxs * other)
operator fun div(other: Vector2d) = AABB(mins / other, maxs / other)
operator fun times(other: Double) = AABB(mins * other, maxs * other)
operator fun div(other: Double) = AABB(mins / other, maxs / other)
val xSpan get() = maxs.x - mins.x
val ySpan get() = maxs.y - mins.y
val centre get() = (mins + maxs) * 0.5
val A get() = mins
val B get() = Vector2d(mins.x, maxs.y)
val C get() = maxs
val D get() = Vector2d(maxs.x, mins.y)
val bottomLeft get() = A
val topLeft get() = B
val topRight get() = C
val bottomRight get() = D
val width get() = maxs.x - mins.x
val height get() = maxs.y - mins.y
val volume get() = max(width * height, 0.0)
val extents get() = Vector2d(width * 0.5, height * 0.5)
val diameter get() = mins.distance(maxs)
val radius get() = diameter / 2.0
val perimeter get() = (xSpan + ySpan) * 2.0
val edges: List<Line2d> by lazy {
immutableList {
accept(Line2d(Vector2d(mins.x, mins.y), Vector2d(maxs.x, mins.y)))
accept(Line2d(Vector2d(maxs.x, mins.y), Vector2d(maxs.x, maxs.y)))
accept(Line2d(Vector2d(maxs.x, maxs.y), Vector2d(mins.x, maxs.y)))
accept(Line2d(Vector2d(mins.x, maxs.y), Vector2d(mins.x, mins.y)))
}
}
fun isInside(point: IStruct2d): Boolean {
return point.component1() in mins.x .. maxs.x && point.component2() in mins.y .. maxs.y
}
fun isInside(x: Double, y: Double): Boolean {
return x in mins.x .. maxs.x && y in mins.y .. maxs.y
}
operator fun contains(point: IStruct2d) = isInside(point)
/**
* Checks whenever is this AABB intersect with [other]
*
* This method consider they intersect even if only one of axis are equal
*/
fun intersect(other: AABB): Boolean {
return intersectRectangles(mins, maxs, other.mins, other.maxs)
}
fun intersect(line: Line2d): Boolean {
return line.p0 in this || line.p1 in this || edges.any { it.intersect(line).intersects }
}
/**
* Returns whenever [other] is contained (encased) inside this AABB
*/
operator fun contains(other: AABB): Boolean {
return rectangleContainsRectangle(mins, maxs, other.mins, other.maxs)
}
/**
* Checks whenever is this AABB intersect with [other]
*
* This method DOES NOT consider they intersect if only one of axis are equal
*/
fun intersectWeak(other: AABB): Boolean {
if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y)
return false
return intersectRectangles(mins, maxs, other.mins, other.maxs)
}
fun intersectionDepth(other: AABB): Vector2d {
val xDepth: Double
val yDepth: Double
val thisCentre = centre
val otherCentre = other.centre
if (thisCentre.x > otherCentre.x) {
// считаем, что мы вошли справа
xDepth = mins.x - other.maxs.x
} else {
// считаем, что мы вошли слева
xDepth = maxs.x - other.mins.x
}
if (thisCentre.y > otherCentre.y) {
// считаем, что мы вошли сверху
yDepth = mins.y - other.maxs.y
} else {
// считаем, что мы вошли снизу
yDepth = maxs.x - other.mins.x
}
return Vector2d(xDepth, yDepth)
}
fun distance(other: AABB): Double {
val intersectX: Boolean
val intersectY: Boolean
if (ySpan <= other.ySpan)
intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
else
intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y
if (xSpan <= other.xSpan)
intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
else
intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x
if (intersectY && intersectX) {
return 0.0
}
if (intersectX) {
return (mins.y - other.maxs.y).absoluteValue.coerceAtMost((maxs.y - other.mins.y).absoluteValue)
} else {
return (mins.x - other.maxs.x).absoluteValue.coerceAtMost((maxs.x - other.mins.x).absoluteValue)
}
}
fun pushOutFrom(other: AABB): Vector2d {
if (!intersect(other))
return Vector2d.ZERO
val depth = intersectionDepth(other)
if (depth.x.absoluteValue < depth.y.absoluteValue) {
return Vector2d(x = depth.x)
} else {
return Vector2d(y = depth.y)
}
}
fun overlap(other: AABB): AABB {
return AABB(
Vector2d(max(mins.x, other.mins.x), max(mins.y, other.mins.y)),
Vector2d(min(maxs.x, other.maxs.x), min(maxs.y, other.maxs.y)),
)
}
/**
* Returns AABB which contains both AABBs
*/
fun combine(other: AABB): AABB {
if (contains(other)) return this
val minX = mins.x.coerceAtMost(other.mins.x)
val minY = mins.y.coerceAtMost(other.mins.y)
val maxX = maxs.x.coerceAtLeast(other.maxs.x)
val maxY = maxs.y.coerceAtLeast(other.maxs.y)
return AABB(Vector2d(minX, minY), Vector2d(maxX, maxY))
}
/**
* Returns AABB which contains this AABB and specified point
*/
fun expand(x: Double, y: Double): AABB {
if (isInside(x, y))
return this
return AABB(
mins.coerceAtMost(x, y),
maxs.coerceAtLeast(x, y)
)
}
fun expand(value: IStruct2d) = expand(value.component1(), value.component2())
fun padded(value: IStruct2d) = expand(value.component1(), value.component2())
fun padded(x: Double, y: Double) = expand(x, y)
/**
* Returns AABB which edges are expanded by [x] and [y] along their normals
*/
fun enlarge(x: Double, y: Double): AABB {
if (x == 0.0 && y == 0.0) return this
return AABB(
Vector2d(mins.x - x, mins.y - y),
Vector2d(maxs.x + x, maxs.y + y),
)
}
fun encasingIntAABB(): AABBi {
return AABBi(
Vector2i(roundByAbsoluteValue(mins.x), roundByAbsoluteValue(mins.y)),
Vector2i(roundByAbsoluteValue(maxs.x), roundByAbsoluteValue(maxs.y)),
)
}
companion object {
/**
* Rectangle with given [width] and [height]
*/
fun rectangle(pos: IStruct2d, width: Double, height: Double = width): AABB {
val (x, y) = pos
return AABB(
Vector2d(x - width / 2.0, y - height / 2.0),
Vector2d(x + width / 2.0, y + height / 2.0),
)
}
/**
* Rectangle with given [width] * 2 and [height] * 2
*/
fun withSide(pos: IStruct2d, width: Double, height: Double = width): AABB {
val (x, y) = pos
return AABB(
Vector2d(x - width, y - height),
Vector2d(x + width, y + height),
)
}
fun leftCorner(pos: Vector2d, width: Double, height: Double): AABB {
return AABB(pos, pos + Vector2d(width, height))
}
fun ofPoints(points: Collection<Vector2d>): AABB {
if (points.isEmpty())
return NEVER
val minX = points.minOf { it.x }
val maxX = points.maxOf { it.x }
val minY = points.minOf { it.y }
val maxY = points.maxOf { it.y }
return AABB(
Vector2d(minX, minY),
Vector2d(maxX, maxY),
)
}
@JvmName("ofPointsI")
fun ofPoints(points: Collection<Vector2i>): AABB {
if (points.isEmpty())
return NEVER
val minX = points.minOf { it.x }.toDouble()
val maxX = points.maxOf { it.x }.toDouble()
val minY = points.minOf { it.y }.toDouble()
val maxY = points.maxOf { it.y }.toDouble()
return AABB(
Vector2d(minX, minY),
Vector2d(maxX, maxY),
)
}
@JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
}
}