All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.korlibs.math.geom.Angle.kt Maven / Gradle / Ivy

There is a newer version: 6.0.0
Show newest version
package korlibs.math.geom

import korlibs.math.*
import korlibs.math.interpolation.*
import korlibs.math.range.*
import korlibs.number.*
import kotlin.math.*

@PublishedApi internal const val PI2 = PI * 2.0
@PublishedApi internal const val DEG2RAD = PI / 180.0
@PublishedApi internal const val RAD2DEG = 180.0 / PI

@PublishedApi internal fun Angle_shortDistanceTo(from: Angle, to: Angle): Angle {
    val r0 = from.ratio.toDouble() umod 1.0
    val r1 = to.ratio.toDouble() umod 1.0
    val diff = (r1 - r0 + 0.5) % 1.0 - 0.5
    return if (diff < -0.5) Angle.fromRatio(diff + 1.0) else Angle.fromRatio(diff)
}

@PublishedApi internal fun Angle_longDistanceTo(from: Angle, to: Angle): Angle {
    val short = Angle_shortDistanceTo(from, to)
    return when {
        short == Angle.ZERO -> Angle.ZERO
        short < Angle.ZERO -> Angle.FULL + short
        else -> -Angle.FULL + short
    }
}

@PublishedApi internal fun Angle_between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle {
    val angle = Angle.atan2(y1 - y0, x1 - x0)
    return (if (angle < Angle.ZERO) angle + Angle.FULL else angle).adjustFromUp(up)
}

@PublishedApi internal fun Angle.adjustFromUp(up: Vector2D): Angle {
    Orientation.checkValidUpVector(up)
    return if (up.y > 0) this else -this
}

/**
 * Represents an [Angle], [ratio] is in [0, 1] range, [radians] is in [0, 2PI] range, and [degrees] in [0, 360] range
 * The internal representation is in [0, 1] range to reduce rounding errors, since floating points can represent
 * a lot of values in that range.
 *
 * The equivalent old [Angle] constructor is now [Angle.fromRadians]
 *
 * Angles advance counter-clock-wise, starting with 0.degrees representing the right vector:
 *
 * Depending on what the up vector means, then numeric values of sin might be negated.
 *
 *   0.degrees represent right:    up=Vector2.UP: cos =+1, sin= 0 || up=Vector2.UP_SCREEN: cos =+1, sin= 0
 *  90.degrees represents up:      up=Vector2.UP: cos = 0, sin=+1 || up=Vector2.UP_SCREEN: cos = 0, sin=-1
 * 180.degrees represents left:    up=Vector2.UP: cos =-1, sin= 0 || up=Vector2.UP_SCREEN: cos =-1, sin= 0
 * 270.degrees represents down:    up=Vector2.UP: cos = 0, sin=-1 || up=Vector2.UP_SCREEN: cos = 0, sin=+1
 */
//@KormaValueApi
inline class Angle @PublishedApi internal constructor(
    /** [0..1] ratio -> [0..360] degrees */
    val radians: Double
) : Comparable, IsAlmostEquals {
    @PublishedApi inline internal val internal: Double get() = radians

    /** [0..PI * 2] radians -> [0..360] degrees */
    val ratio: Ratio get() = radiansToRatio(radians)
    /** [0..360] degrees -> [0..PI * 2] radians -> [0..1] ratio */
    val degrees: Double get() = radiansToDegrees(radians)

    val cosine: Double get() = kotlin.math.cos(radians)
    val sine: Double get() = kotlin.math.sin(radians)
    val tangent: Double get() = kotlin.math.tan(radians)

    fun cosine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).cosine
    fun sine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).sine
    fun tangent(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).tangent

    val absoluteValue: Angle get() = Angle(internal.absoluteValue)
    fun shortDistanceTo(other: Angle): Angle = Angle.shortDistanceTo(this, other)
    fun longDistanceTo(other: Angle): Angle = Angle.longDistanceTo(this, other)

    operator fun times(scale: Double): Angle = Angle(this.internal * scale)
    operator fun div(scale: Double): Angle = Angle(this.internal / scale)
    operator fun times(scale: Float): Angle = Angle(this.internal * scale)
    operator fun div(scale: Float): Angle = Angle(this.internal / scale)
    operator fun times(scale: Int): Angle = Angle(this.internal * scale)
    operator fun div(scale: Int): Angle = Angle(this.internal / scale)
    operator fun rem(angle: Angle): Angle = Angle(this.internal % angle.internal)
    infix fun umod(angle: Angle): Angle = Angle(this.internal umod angle.internal)

    operator fun div(other: Angle): Double = this.internal / other.internal // Ratio
    operator fun plus(other: Angle): Angle = Angle(this.internal + other.internal)
    operator fun minus(other: Angle): Angle = Angle(this.internal - other.internal)
    operator fun unaryMinus(): Angle = Angle(-internal)
    operator fun unaryPlus(): Angle = Angle(+internal)

    fun inBetweenInclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = true)
    fun inBetweenExclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = false)

    infix fun inBetween(range: ClosedRange): Boolean = inBetween(range.start, range.endInclusive, inclusive = true)
    infix fun inBetween(range: OpenRange): Boolean = inBetween(range.start, range.endExclusive, inclusive = false)

    fun inBetween(min: Angle, max: Angle, inclusive: Boolean): Boolean {
        val nthis = this.normalized
        val nmin = min.normalized
        val nmax = max.normalized
        @Suppress("ConvertTwoComparisonsToRangeCheck")
        return when {
            nmin > nmax -> nthis >= nmin || (if (inclusive) nthis <= nmax else nthis < nmax)
            else -> nthis >= nmin && (if (inclusive) nthis <= nmax else nthis < nmax)
        }
    }

    override fun isAlmostEquals(other: Angle, epsilon: Double): Boolean = this.radians.isAlmostEquals(other.radians, epsilon)
    fun isAlmostZero(epsilon: Double = 0.001): Boolean = isAlmostEquals(ZERO, epsilon)

    /** Normalize between 0..1  ... 0..(PI*2).radians ... 0..360.degrees */
    val normalized: Angle get() = fromRatio(ratio.toDouble() umod 1.0)
    /** Normalize between -.5..+.5  ... -PI..+PI.radians ... -180..+180.degrees */
    val normalizedHalf: Angle get() {
        val res = normalized
        return if (res > Angle.HALF) -Angle.FULL + res else res
    }

    override operator fun compareTo(other: Angle): Int = this.ratio.compareTo(other.ratio)

    //override fun compareTo(other: Angle): Int {
    //    //return this.radians.compareTo(other.radians) // @TODO: Double.compareTo calls EnterFrame/LeaveFrame! because it uses a Double companion object
    //    val left = this.ratio
    //    val right = other.ratio
    //    // @TODO: Handle infinite/NaN? Though usually this won't happen
    //    if (left < right) return -1
    //    if (left > right) return +1
    //    return 0
    //}

    override fun toString(): String = "${degrees.roundDecimalPlaces(2).niceStr}.degrees"

    @Suppress("MemberVisibilityCanBePrivate")
    companion object {
        val EPSILON = Angle.fromRatio(0.00001)
        val ZERO = Angle.fromRatio(0.0)
        val QUARTER = Angle.fromRatio(0.25)
        val HALF = Angle.fromRatio(0.5)
        val THREE_QUARTERS = Angle.fromRatio(0.75)
        val FULL = Angle.fromRatio(1.0)

        inline fun fromRatio(ratio: Float): Angle = Angle(ratioToRadians(ratio.toRatio()))
        inline fun fromRatio(ratio: Double): Angle = Angle(ratioToRadians(ratio.toRatio()))
        inline fun fromRatio(ratio: Ratio): Angle = Angle(ratioToRadians(ratio))

        inline fun fromRadians(radians: Double): Angle = Angle(radians)
        inline fun fromRadians(radians: Float) = Angle(radians.toDouble())
        inline fun fromRadians(radians: Int) = Angle(radians.toDouble())

        inline fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees))
        inline fun fromDegrees(degrees: Float) = Angle(degreesToRadians(degrees.toDouble()))
        inline fun fromDegrees(degrees: Int) = Angle(degreesToRadians(degrees.toDouble()))

        @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).cosineD"))
        inline fun cos01(ratio: Double): Double = Angle.fromRatio(ratio).cosine
        @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).sineD"))
        inline fun sin01(ratio: Double): Double = Angle.fromRatio(ratio).sine
        @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).tangentD"))
        inline fun tan01(ratio: Double): Double = Angle.fromRatio(ratio).tangent

        inline fun atan2(x: Float, y: Float, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
        inline fun atan2(x: Double, y: Double, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up)
        inline fun atan2(p: Point, up: Vector2D = Vector2D.UP): Angle = atan2(p.x, p.y, up)

        inline fun asin(v: Double): Angle = kotlin.math.asin(v).radians
        inline fun asin(v: Float): Angle = kotlin.math.asin(v).radians

        inline fun acos(v: Double): Angle = kotlin.math.acos(v).radians
        inline fun acos(v: Float): Angle = kotlin.math.acos(v).radians

        fun arcCosine(v: Double): Angle = kotlin.math.acos(v).radians
        fun arcCosine(v: Float): Angle = kotlin.math.acos(v).radians

        fun arcSine(v: Double): Angle = kotlin.math.asin(v).radians
        fun arcSine(v: Float): Angle = kotlin.math.asin(v).radians

        fun arcTangent(x: Double, y: Double): Angle = kotlin.math.atan2(x, y).radians
        fun arcTangent(x: Float, y: Float): Angle = kotlin.math.atan2(x, y).radians
        fun arcTangent(v: Vector2F): Angle = kotlin.math.atan2(v.x, v.y).radians

        inline fun ratioToDegrees(ratio: Ratio): Double = ratio * 360.0
        inline fun ratioToRadians(ratio: Ratio): Double = ratio * PI2

        inline fun degreesToRatio(degrees: Double): Ratio = Ratio(degrees / 360.0)
        inline fun degreesToRadians(degrees: Double): Double = degrees * DEG2RAD

        inline fun radiansToRatio(radians: Double): Ratio = Ratio(radians / PI2)
        inline fun radiansToDegrees(radians: Double): Double = radians * RAD2DEG

        inline fun shortDistanceTo(from: Angle, to: Angle): Angle = Angle_shortDistanceTo(from, to)
        inline fun longDistanceTo(from: Angle, to: Angle): Angle = Angle_longDistanceTo(from, to)
        inline fun between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle = Angle_between(x0, y0, x1, y1, up)

        inline fun between(x0: Int, y0: Int, x1: Int, y1: Int, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)
        inline fun between(x0: Float, y0: Float, x1: Float, y1: Float, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up)

        inline fun between(p0: Point, p1: Point, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)
        inline fun between(p0: Vector2F, p1: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up)

        inline fun between(ox: Double, oy: Double, x1: Double, y1: Double, x2: Double, y2: Double, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)
        inline fun between(ox: Float, oy: Float, x1: Float, y1: Float, x2: Float, y2: Float, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up)

        inline fun between(o: Point, v1: Point, v2: Point, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
        inline fun between(o: Vector2F, v1: Vector2F, v2: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up)
    }
}

inline fun cos(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.cosine(up)
inline fun sin(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.sine(up)
inline fun tan(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.tangent(up)

inline fun cosf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.cosine(up).toFloat()
inline fun sinf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.sine(up).toFloat()
inline fun tanf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.tangent(up).toFloat()

inline fun abs(angle: Angle): Angle = angle.absoluteValue
inline fun min(a: Angle, b: Angle): Angle = Angle(min(a.internal, b.internal))
inline fun max(a: Angle, b: Angle): Angle = Angle(max(a.internal, b.internal))

fun Angle.clamp(min: Angle, max: Angle): Angle = min(max(this, min), max)

operator fun ClosedRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endInclusive, inclusive = true)
operator fun OpenRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endExclusive, inclusive = false)
infix fun Angle.until(other: Angle): OpenRange = OpenRange(this, other)

val Double.degrees: Angle get() = Angle.fromDegrees(this)
val Double.radians: Angle get() = Angle.fromRadians(this)
val Int.degrees: Angle get() = Angle.fromDegrees(this)
val Int.radians: Angle get() = Angle.fromRadians(this)
val Float.degrees: Angle get() = Angle.fromDegrees(this)
val Float.radians: Angle get() = Angle.fromRadians(this)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy