Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable Vector 2D #161

Merged
merged 40 commits into from
Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
08b121c
Add KVector and KVector2
jcornaz Aug 13, 2018
05b49d9
Merge branch 'master' into feature/immutable-vectors
jcornaz Sep 30, 2018
0746b62
Rename KVector to ImmutableVector
jcornaz Oct 1, 2018
2d4eb3c
Test with small vectors
jcornaz Oct 1, 2018
98823c4
inline alias functions and solve few warnings
jcornaz Oct 1, 2018
7403c9d
Provide angleDeg() and deprecate angle()
jcornaz Oct 2, 2018
429e094
Improve documentation
jcornaz Oct 2, 2018
1f66974
Update changelog and start to write the readme
jcornaz Oct 2, 2018
58c4cbb
Merge branch 'develop' into feature/immutable-vectors
jcornaz Oct 2, 2018
7d9f0c4
Port syntax sugars provided by ktx for `Vector2`
jcornaz Oct 2, 2018
9ef346d
Fix some typos
jcornaz Oct 2, 2018
446afb2
Add module name in changelog
jcornaz Oct 3, 2018
008693f
Minor clean up
jcornaz Oct 3, 2018
c388137
Explicitly test equals and hashCode
jcornaz Oct 3, 2018
f1d51eb
Test destructuring immutable vector
jcornaz Oct 3, 2018
ca6364b
Make setLength2 and extension function
jcornaz Oct 3, 2018
71875f7
Address some minor issues
jcornaz Oct 4, 2018
d4b51a7
Rename conversion functions (`toMutable` and `toImmutable`)
jcornaz Oct 4, 2018
9de52b1
Fix typos in tests
jcornaz Oct 4, 2018
ac08485
Add mutation method deprecated to allow smoother migration
jcornaz Oct 4, 2018
9a19a62
Explicitly test the usage of method `copy` and update readme.
jcornaz Oct 4, 2018
c4d8a2e
Increase deprecation level to ERROR for mutation methods
jcornaz Oct 4, 2018
9279fee
Change deprecation reason and level of `rotate`
jcornaz Oct 4, 2018
1c0070e
Update new names of functions in tests names
jcornaz Oct 4, 2018
063a017
Fix deprecation message and level of `cpy`
jcornaz Oct 4, 2018
455ce76
Add examples in readme
jcornaz Oct 7, 2018
4d29ad3
Fix typo in readme
jcornaz Oct 7, 2018
dbdf93b
Improve documentation
jcornaz Oct 27, 2018
b95c6ad
Fix misleading documentation of result vectors
jcornaz Oct 27, 2018
cdc72c9
Add some tests
jcornaz Oct 27, 2018
06909b8
test withClamp and withClamp2
jcornaz Oct 27, 2018
90f7186
Add few tests for angles
jcornaz Nov 17, 2018
c1bb643
Add direction comparison tests
jcornaz Nov 18, 2018
99b02c8
Add tests for commutativity when relevant
jcornaz Nov 18, 2018
18d87eb
Add more tests for interpolations
jcornaz Nov 18, 2018
3cb53ad
Merge branch 'develop' into feature/immutable-vectors
jcornaz Dec 5, 2018
a9456b7
Change signature and update documentation of ImmutableVector2.time(Af…
jcornaz Dec 11, 2018
43c9e58
Address small typo issues
jcornaz Dec 11, 2018
6997b62
Use fixed seed when testing withRandomDirection
jcornaz Dec 11, 2018
8a58587
Add reference to arguments in documentations
jcornaz Dec 11, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### 1.9.8-SNAPSHOT
- **[FEATURE]** (`ktx-math`) Added `ImmutableVector2`, an immutable equivalent to `Vector2`.

#### 1.9.8-b5

Expand Down
22 changes: 22 additions & 0 deletions math/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ Note that since `Shape2D` has `contains(Vector2)` method, `in` operator can be u
(like `Rectangle`, `Ellipse` or `Circle`). For example, given `vec: Vector2` and `rect: Rectangle` variables, you can
call `vec in rect` (or `vec !in rect`) to check if the rectangle contains (or doesn't) the point stored by the vector.

#### `ImmutableVector2`
- `ImmutableVector2` is an immutable equivalent to `Vector2`. It provides most of the functionality of `Vector2`, but
mutation methods return new vectors instead of mutate the reference.
- Note that one may want to create type aliases to makes the usage more concise: `typealias Vect2 = ImmutableVector2`
- `ImmutableVector` is comparable (`>`, `>=`, `<`, `<=` are available). Comparison is evaluated by length
- instances can be destructed: `val (x, y) = vector2`
- `Vector2.toImmutableVector2()` Returns an immutable vector with same `x` and `y` attributes than this `Vector2`
- `ImmutableVector2.toVector2()` Returns an mutable vector with same `x` and `y` attributes than this `ImmutableVector2`
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
- Notable difference with `Vector2`
- `+`, `-`, `*`, `/` are available and replace `add`, `sub` and `scl`.
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
- `withLength()` and `withLength2()` replace `setLength()` and `setLength2()` and return a new vector of same direction
with the specified length
- `withRandomRotation` replace `setToRandomRotation` and return a new vector of same length and a random rotation
- `withAngleDeg()` and `withAngleRad` replace `setAngle` and `setAngleRad` and return a new vector of same length and
the given angle to x-axis
- `cpy` is not provided and is not necessary. Immutable vectors can be safely shared.
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
- `set(x, y)` and `setZero()` are not provided.
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
- Functions dealing with angles in degree are suffixed with `Deg` and all returns values between `-180` and `+180`.
- All angle functions return the angle toward positive y-axis.
- `dot` is an infix function
- `x` and `crs` infix functions replace `crs` (cross product)

#### `Vector3`

- `vec3` is a global factory function that can create `Vector3` instances with named parameters for extra readability.
Expand Down
2 changes: 2 additions & 0 deletions math/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
dependencies {
provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"

testCompile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
}
154 changes: 154 additions & 0 deletions math/src/main/kotlin/ktx/math/ImmutableVector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
@file:Suppress("NOTHING_TO_INLINE")

package ktx.math

import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.MathUtils
import com.badlogic.gdx.math.Vector
import kotlin.math.abs
import kotlin.math.sqrt

/**
* Represent an immutable vector.
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
*/
interface ImmutableVector<T : ImmutableVector<T>> : Comparable<T> {

/**
* Returns the squared euclidean length
*
* This method is faster than [Vector.len] because it avoids calculating a square root. It is useful for comparisons,
* but not for getting exact lengths, as the return value is the square of the actual length.
*/
val len2: Float

/** Returns whether the length of this vector is smaller than the given [margin] */
fun isZero(margin: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean

/** Returns the result of subtracting the [other] vector from this vector */
operator fun minus(other: T): T

/** Returns the result of adding the [other] vector to this vector */
operator fun plus(other: T): T

/** Returns the same vector with all members incremented by 1 */
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
operator fun inc(): T

/** Returns the same vector with all members decremented by 1 */
operator fun dec(): T

/** Returns this vector scaled by the given [scalar] */
operator fun times(scalar: Float): T

/** Returns his vector scaled by the given [vector] */
operator fun times(vector: T): T

/** Returns he dot product of this vector by the given [vector] */
infix fun dot(vector: T): Float

/**
* Returns the squared distance between this and the other vector
*
* This method is faster than [dst] because it avoids calculating a square root. It is useful for comparisons,
* but not for getting exact distance, as the return value is the square of the actual distance.
*/
infix fun dst2(vector: T): Float

/** Linearly interpolates between this vector and the target vector by alpha */
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
fun lerp(target: T, alpha: Float): T

/** Returns a vector of same length and a random direction */
fun withRandomDirection(): T

/** Returns true if this vector is in line with the other vector (either in the same or the opposite direction) */
fun isOnLine(other: T, epsilon: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean

/** Compares this vector with the other vector, using the supplied epsilon for fuzzy equality testing */
fun epsilonEquals(other: T, epsilon: Float): Boolean

override fun compareTo(other: T): Int = len2.compareTo(other.len2)
}

/**
* Returns the euclidean length
*/
inline val ImmutableVector<*>.len: Float get() = sqrt(len2)
czyzby marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns the unit vector of same direction or this vector if it is zero.
*/
val <T : ImmutableVector<T>> T.nor: T
get() {
val l2 = len2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename the l2 parameter to lengthSquared for readability? I want the sources to be easy to read for Kotlin/LibGDX beginners.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed for the folowing:

override val nor: ImmutableVector2 get() = withLength2(1f)

override fun withLength2(length2: Float): ImmutableVector2 {
  val oldLen2 = this.len2

  return if (oldLen2 == 0f || oldLen2 == length2) this else times(sqrt(length2 / oldLen2))
}

Is that ok?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd generally tend to rewrite the conditionals to end up with else this, but it's subjective. Since there are two checks and slightly nested series of calls, I'd consider breaking the one-liner and using braces. Something along the lines of:

return if (oldLen2 != 0f && oldLen2 != length2) {
  times(sqrt(length2 / oldLen2))
} else this


if (l2 == 1f || l2 == 0f) return this
jcornaz marked this conversation as resolved.
Show resolved Hide resolved

return withLength2(1f)
}

/** Returns a vector of the same direction and a squared length of [length2] */
fun <T : ImmutableVector<T>> T.withLength2(length2: Float): T {
val oldLen2 = this.len2

if (oldLen2 == 0f || oldLen2 == length2) return this

return times(sqrt(length2 / oldLen2))
}

/** Returns whether this vector is a unit length vector within the given [margin]. (no margin by default) */
inline fun <T : ImmutableVector<T>> T.isUnit(margin: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean = abs(1f - len2) < margin

/** Returns the distance between this and the [other] vector */
inline infix fun <T : ImmutableVector<T>> T.dst(other: T): Float = sqrt(dst2(other))

/** Returns this vector scaled by (1 / [scalar]) */
inline operator fun <T : ImmutableVector<T>> T.div(scalar: Float): T = times(1 / scalar)

/** Returns this vector if the [ImmutableVector.len] is <= [limit] or a vector with the same direction and length [limit] otherwise */
inline fun <T : ImmutableVector<T>> T.limit(limit: Float): T = limit2(limit * limit)
czyzby marked this conversation as resolved.
Show resolved Hide resolved

/** Returns this vector if the [ImmutableVector.len2] is <= [limit2] or a vector with the same direction and length [limit2] otherwise */
fun <T : ImmutableVector<T>> T.limit2(limit2: Float): T =
if (len2 <= limit2) this else withLength2(limit2)

/** Clamps this vector's length to given [min] and [max] values*/
inline fun <T : ImmutableVector<T>> T.clamp(min: Float, max: Float): T = clamp2(min * min, max * max)

/** Clamps this vector's squared length to given [min2] and [max2] values*/
fun <T : ImmutableVector<T>> T.clamp2(min2: Float, max2: Float): T {
val l2 = len2
return when {
l2 < min2 -> withLength2(min2)
l2 > max2 -> withLength2(max2)
else -> this
}
}

/** Returns a vector of same direction and the given [length] */
inline fun <T : ImmutableVector<T>> T.withLength(length: Float): T = withLength2(length * length)

/** Returns the opposite vector of same length */
inline operator fun <T : ImmutableVector<T>> T.unaryMinus(): T = times(-1f)

/** Interpolates between this vector and the given [target] vector by [alpha] (within range [0,1]) using the given [interpolation] method. */
inline fun <T : ImmutableVector<T>> T.interpolate(target: T, alpha: Float, interpolation: Interpolation): T =
lerp(target, interpolation.apply(alpha))

/** Returns true if this vector is collinear with the [other] vector */
inline fun <T : ImmutableVector<T>> T.isCollinear(other: T, epsilon: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean =
isOnLine(other, epsilon) && hasSameDirection(other)

/** Returns true if this vector is opposite collinear with the [other] vector */
inline fun <T : ImmutableVector<T>> T.isCollinearOpposite(other: T, epsilon: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean =
isOnLine(other, epsilon) && hasOppositeDirection(other)

/** Returns true if this vector is opposite perpendicular with the [other] vector */
inline fun <T : ImmutableVector<T>> T.isPerpendicular(other: T, epsilon: Float = MathUtils.FLOAT_ROUNDING_ERROR): Boolean =
MathUtils.isZero(dot(other), epsilon)

/** Returns whether this vector has similar direction compared to the other vector. */
inline fun <T : ImmutableVector<T>> T.hasSameDirection(other: T): Boolean =
dot(other) > 0f

/** Returns whether this vector has opposite direction compared to the other vector. */
inline fun <T : ImmutableVector<T>> T.hasOppositeDirection(other: T): Boolean =
dot(other) < 0f
182 changes: 182 additions & 0 deletions math/src/main/kotlin/ktx/math/ImmutableVector2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
@file:Suppress("NOTHING_TO_INLINE")

package ktx.math

import com.badlogic.gdx.math.MathUtils
import com.badlogic.gdx.math.Matrix3
import com.badlogic.gdx.math.Vector2
import kotlin.math.*

/**
* Represent an **immutable** vector
*
* @property x the x-component of this vector
* @property y the y-component of this vector
*/
data class ImmutableVector2(val x: Float, val y: Float) : ImmutableVector<ImmutableVector2> {

override val len2: Float = Vector2.len2(x, y)

override fun isZero(margin: Float): Boolean = (x == 0f && y == 0f) || len2 < margin

override operator fun minus(other: ImmutableVector2): ImmutableVector2 = minus(other.x, other.y)

/** Returns the result of subtracting the given vector from this vector */
fun minus(deltaX: Float = 0f, deltaY: Float = 0f): ImmutableVector2 = ImmutableVector2(x - deltaX, y - deltaY)

override operator fun plus(other: ImmutableVector2): ImmutableVector2 = plus(other.x, other.y)

/** Returns the result of adding the given vector from this vector */
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
fun plus(deltaX: Float = 0f, deltaY: Float = 0f): ImmutableVector2 = ImmutableVector2(x + deltaX, y + deltaY)

override fun inc(): ImmutableVector2 = copy(x = x + 1, y = y + 1)
override fun dec(): ImmutableVector2 = copy(x = x - 1, y = y - 1)
jcornaz marked this conversation as resolved.
Show resolved Hide resolved

override operator fun times(scalar: Float): ImmutableVector2 = times(scalar, scalar)
override operator fun times(vector: ImmutableVector2): ImmutableVector2 = times(vector.x, vector.y)

/** Returns this vector scaled by the given [xf] and [yf] factors */
fun times(xf: Float, yf: Float): ImmutableVector2 = ImmutableVector2(x * xf, y * yf)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that these were the method parameters in the original LibGDX code, but I'm not a fan of such non-obvious names. You could use x, factorX or xFactor instead.

Copy link
Contributor Author

@jcornaz jcornaz Oct 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of such non-obvious names

Me neither. Actually in LibDGX they were named x and y. But i tend to find it even less obvious.

I'll go for factorX. (same for the methods bellow)


override fun dot(vector: ImmutableVector2): Float = dot(vector.x, vector.y)

/** Returns the dot product of this vector by the given vector */
fun dot(ox: Float, oy: Float): Float = Vector2.dot(x, y, ox, oy)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, non-obvious names. x, otherX?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes for a few methods below.


override fun dst2(vector: ImmutableVector2): Float = dst2(vector.x, vector.y)

/**
* Returns the squared distance between this and the other vector
*
* This method is faster than [dst] because it avoids calculating a square root. It is useful for comparisons,
* but not for getting exact distance, as the return value is the square of the actual distance.
*/
fun dst2(ox: Float, oy: Float): Float = Vector2.dst2(x, y, ox, oy)

/** @return the distance between this and the other vector */
fun dst(ox: Float, oy: Float): Float = Vector2.dst(x, y, ox, oy)

operator fun times(matrix: Matrix3): ImmutableVector2 = ImmutableVector2(
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
x = x * matrix.`val`[0] + y * matrix.`val`[3] + matrix.`val`[6],
y = x * matrix.`val`[1] + y * matrix.`val`[4] + matrix.`val`[7]
)

/** Calculates the 2D cross product between this and the given vector */
fun crs(x: Float, y: Float): Float = this.x * y - this.y * x

/** Returns the angle in radians of this vector relative to the [reference]. Angles are towards the positive y-axis. (typically counter-clockwise) */
fun angleRad(reference: ImmutableVector2 = ImmutableVector2.X): Float = angleRad(reference.x, reference.y)

/** Returns the angle in radians of this vector relative to the reference. Angles are towards the positive y-axis. (typically counter-clockwise) */
fun angleRad(ox: Float, oy: Float): Float {
val result = atan2(y, x) - atan2(oy, ox)
return when {
result > MathUtils.PI -> result - MathUtils.PI2
result < -MathUtils.PI -> result + MathUtils.PI2
else -> result
}
}

/** Returns a vector of same length with the given angle in radians */
fun withAngleRad(radians: Float): ImmutableVector2 = ImmutableVector2(len, 0f).rotateRad(radians)

/** Returns a vector of same length rotated by the given [angle] in radians */
fun rotateRad(angle: Float): ImmutableVector2 {
val cos = cos(angle)
val sin = sin(angle)

return ImmutableVector2(
x = this.x * cos - this.y * sin,
y = this.x * sin + this.y * cos
)
}

override fun lerp(target: ImmutableVector2, alpha: Float): ImmutableVector2 {
val invAlpha = 1.0f - alpha

return ImmutableVector2(
x = x * invAlpha + target.x * alpha,
y = y * invAlpha + target.y * alpha
)
}

override fun withRandomDirection(): ImmutableVector2 = withAngleRad(MathUtils.random(0f, MathUtils.PI2))

override fun epsilonEquals(other: ImmutableVector2, epsilon: Float): Boolean =
epsilonEquals(other.x, other.y, epsilon)

/** Compares this vector with the other vector, using the supplied epsilon for fuzzy equality testing */
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
fun epsilonEquals(x: Float, y: Float, epsilon: Float = Float.MIN_VALUE): Boolean =
abs(x - this.x) <= epsilon && abs(y - this.y) <= epsilon

override fun isOnLine(other: ImmutableVector2, epsilon: Float): Boolean =
MathUtils.isZero(x * other.y - y * other.x, epsilon)

override fun toString(): String = "($x,$y)"

@Deprecated(
message = "This function doesn't behave like its equivalent in LibGDX and return an angle between -180 and 180 (some LibGDX functions return between -180 and 180 and some other between 0 and 360)",
replaceWith = ReplaceWith("angleDeg(reference)")
)
inline fun angle(reference: ImmutableVector2 = ImmutableVector2.X): Float = angleDeg(reference)

@Deprecated(
message = "use rotateDeg instead. (this function is not provided to be consistent with angleDeg)",
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
replaceWith = ReplaceWith("rotateDeg(angle)")
)
inline fun rotate(angle: Float): ImmutableVector2 = rotateDeg(angle)

companion object {

/** Vector zero */
val ZERO = ImmutableVector2(0f, 0f)

/** Unit vector of positive x axis */
val X = ImmutableVector2(1f, 0f)

/** Unit vector of positive y axis */
val Y = ImmutableVector2(0f, 1f)

/**
* Returns the [ImmutableVector2] represented by the specified string according to the format of [ImmutableVector2::toString]
*/
fun fromString(string: String): ImmutableVector2 =
Vector2().fromString(string).toImmutableVector2()
}
}

/** @return an instance of [ImmutableVector2] with the same x and y values */
inline fun ImmutableVector2.toVector2(): Vector2 = Vector2(x, y)
jcornaz marked this conversation as resolved.
Show resolved Hide resolved

/** @return an instance of [Vector2] with the same x and y values */
inline fun Vector2.toImmutableVector2(): ImmutableVector2 = ImmutableVector2(x, y)
jcornaz marked this conversation as resolved.
Show resolved Hide resolved

/** Returns the angle in degrees of this vector relative to the [reference]. Angles are towards the positive y-axis (typically counter-clockwise.) between -180 and +180 */
inline fun ImmutableVector2.angleDeg(reference: ImmutableVector2 = ImmutableVector2.X): Float =
angleDeg(reference.x, reference.y)

/** Returns the angle in degrees of this vector relative to the reference vector described by [x] and [y]. Angles are towards the positive y-axis (typically counter-clockwise.) between -180 and +180 */
inline fun ImmutableVector2.angleDeg(x: Float, y: Float): Float =
angleRad(x, y) * MathUtils.radiansToDegrees

/** Returns a vector of same length rotated by the given [angle] in degree */
inline fun ImmutableVector2.rotateDeg(angle: Float): ImmutableVector2 =
rotateRad(angle * MathUtils.degreesToRadians)

/**
* Returns a vector of same length rotated by 90 degrees in the given [dir]
*
* @param dir positive value means toward positive y-axis (typically counter-clockwise). Negative value means toward negative y-axis (typically clockwise).
*/
inline fun ImmutableVector2.rotate90(dir: Int): ImmutableVector2 =
jcornaz marked this conversation as resolved.
Show resolved Hide resolved
if (dir >= 0) copy(x = -y, y = x) else copy(x = y, y = -x)

/** Returns a vector of same length with the given [angle] in degree */
inline fun ImmutableVector2.withAngleDeg(angle: Float): ImmutableVector2 =
withAngleRad(angle * MathUtils.degreesToRadians)

/** Calculates the 2D cross product between this and the [other] vector */
inline infix fun ImmutableVector2.x(other: ImmutableVector2): Float = crs(other.x, other.y)

/** Calculates the 2D cross product between this and the [other] vector */
inline infix fun ImmutableVector2.crs(other: ImmutableVector2): Float = crs(other.x, other.y)
jcornaz marked this conversation as resolved.
Show resolved Hide resolved