1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.ink.geometry 18 19 import androidx.annotation.FloatRange 20 import androidx.annotation.RestrictTo 21 import androidx.ink.geometry.internal.VecNative 22 import kotlin.math.abs 23 import kotlin.math.atan2 24 import kotlin.math.hypot 25 26 /** 27 * A two-dimensional vector, i.e. an (x, y) coordinate pair. It can be used to represent either: 28 * 1) A two-dimensional offset, i.e. the difference between two points 29 * 2) A point in space, i.e. treating the vector as an offset from the origin 30 */ 31 public abstract class Vec internal constructor() { 32 /** The [Vec]'s offset in the x-direction */ 33 public abstract val x: Float 34 35 /** The [Vec]'s offset in the y-direction */ 36 public abstract val y: Float 37 38 /** The length of the [Vec]. */ computeMagnitudenull39 @FloatRange(from = 0.0) public fun computeMagnitude(): Float = hypot(x, y) 40 41 /** The squared length of the [Vec]. */ 42 @FloatRange(from = 0.0) public fun computeMagnitudeSquared(): Float = x * x + y * y 43 44 /** 45 * The direction of the vec, represented as the angle between the positive x-axis and this vec. 46 * If either component of the vector is NaN, this returns a NaN angle; otherwise, the returned 47 * value will lie in the interval [-π, π], and will have the same sign as the vector's 48 * y-component. 49 * 50 * Following the behavior of `atan2`, this will return either ±0 or ±π for the zero vector, 51 * depending on the signs of the zeros. 52 */ 53 @FloatRange(from = -Math.PI, to = Math.PI) 54 @AngleRadiansFloat 55 public fun computeDirection(): Float = atan2(y, x) 56 57 /** 58 * Returns a newly allocated vector with the same direction as this one, but with a magnitude of 59 * `1`. This is equivalent to (but faster than) calling [ImmutableVec.fromDirectionAndMagnitude] 60 * with [computeDirection] and `1`. 61 * 62 * In keeping with the above equivalence, this will return <±1, ±0> for the zero vector, 63 * depending on the signs of the zeros. 64 * 65 * For performance-sensitive code, use [computeUnitVec] with a pre-allocated instance of 66 * [MutableVec]. 67 */ 68 public fun computeUnitVec(): ImmutableVec = 69 VecNative.unitVec(this.x, this.y, ImmutableVec::class.java) 70 71 /** 72 * Modifies [outVec] into a vector with the same direction as this one, but with a magnitude of 73 * `1`. Returns [outVec]. This is equivalent to (but faster than) calling 74 * [MutableVec.fromDirectionAndMagnitude] with [computeDirection] and `1`. 75 * 76 * In keeping with the above equivalence, this will return <±1, ±0> for the zero vector, 77 * depending on the signs of the zeros. 78 */ 79 public fun computeUnitVec(outVec: MutableVec): MutableVec { 80 VecNative.populateUnitVec(x, y, outVec) 81 return outVec 82 } 83 84 /** 85 * Returns a newly allocated vector with the same magnitude as this one, but rotated by 86 * (positive) 90 degrees. For performance-sensitive code, use [computeOrthogonal] with a 87 * pre-allocated instance of [MutableVec]. 88 */ computeOrthogonalnull89 public fun computeOrthogonal(): ImmutableVec = ImmutableVec(-y, x) 90 91 /** 92 * Modifies [outVec] into a vector with the same magnitude as this one, but rotated by 93 * (positive) 90 degrees. Returns [outVec]. 94 */ 95 public fun computeOrthogonal(outVec: MutableVec): MutableVec { 96 outVec.x = -y 97 outVec.y = x 98 return outVec 99 } 100 101 /** 102 * Returns a newly allocated vector with the same magnitude, but pointing in the opposite 103 * direction. For performance-sensitive code, use [computeNegation] with a pre-allocated 104 * instance of [MutableVec]. 105 */ computeNegationnull106 public fun computeNegation(): ImmutableVec = ImmutableVec(-x, -y) 107 108 /** 109 * Modifies [outVec] into a vector with the same magnitude, but pointing in the opposite 110 * direction. Returns [outVec]. 111 */ 112 public fun computeNegation(outVec: MutableVec): MutableVec { 113 outVec.x = -x 114 outVec.y = -y 115 return outVec 116 } 117 118 /** 119 * Returns an immutable copy of this object. This will return itself if called on an immutable 120 * instance. 121 */ asImmutablenull122 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract fun asImmutable(): ImmutableVec 123 124 /** 125 * Returns true if the angle formed by `this` and [other] is within [angleTolerance] of 0 126 * radians or π radians (0 degrees or 180 degrees). 127 */ 128 public fun isParallelTo( 129 other: Vec, 130 @AngleRadiansFloat @FloatRange(from = 0.0) angleTolerance: Float, 131 ): Boolean { 132 val absoluteAngle = absoluteAngleBetween(this, other) 133 return absoluteAngle < angleTolerance || Math.PI - absoluteAngle < angleTolerance 134 } 135 136 /** 137 * Returns true if the angle formed by `this` and [other] is within [angleTolerance] of ±π/2 138 * radians (±90 degrees). 139 */ isPerpendicularTonull140 public fun isPerpendicularTo( 141 other: Vec, 142 @AngleRadiansFloat @FloatRange(from = 0.0) angleTolerance: Float, 143 ): Boolean { 144 return abs(absoluteAngleBetween(this, other) - (Math.PI / 2)) < angleTolerance 145 } 146 147 /** 148 * Compares this [Vec] with [other], and returns true if the difference between [x] and 149 * [other.x] is less than [tolerance], and likewise for [y]. 150 */ 151 @JvmOverloads isAlmostEqualnull152 public fun isAlmostEqual( 153 other: Vec, 154 @FloatRange(from = 0.0) tolerance: Float = 0.0001f, 155 ): Boolean = (abs(x - other.x) < tolerance) && (abs(y - other.y) < tolerance) 156 157 public companion object { 158 159 /** The origin of the coordinate system, i.e. (0, 0). */ 160 @JvmField public val ORIGIN: ImmutableVec = ImmutableVec(0f, 0f) 161 162 /** Adds the x and y values of both [Vec] objects and stores the result in [output]. */ 163 @JvmStatic 164 public fun add(lhs: Vec, rhs: Vec, output: MutableVec) { 165 output.x = lhs.x + rhs.x 166 output.y = lhs.y + rhs.y 167 } 168 169 /** 170 * Subtracts the x and y values of [rhs] from the x and y values of [lhs] and stores the 171 * result in [output]. 172 */ 173 @JvmStatic 174 public fun subtract(lhs: Vec, rhs: Vec, output: MutableVec) { 175 output.x = lhs.x - rhs.x 176 output.y = lhs.y - rhs.y 177 } 178 179 /** 180 * Multiplies the x and y values of the [Vec] by the Float and stores the result in 181 * [output]. 182 */ 183 @JvmStatic 184 public fun multiply(lhs: Vec, rhs: Float, output: MutableVec) { 185 output.x = lhs.x * rhs 186 output.y = lhs.y * rhs 187 } 188 189 /** 190 * Multiplies the x and y values of the [Vec] by the Float and stores the result in 191 * [output]. 192 */ 193 @JvmStatic 194 public fun multiply(lhs: Float, rhs: Vec, output: MutableVec) { 195 multiply(rhs, lhs, output) 196 } 197 198 /** 199 * Divides the x and y values of the [Vec] by the Float and stores the result in [output]. 200 */ 201 @JvmStatic 202 public fun divide(lhs: Vec, rhs: Float, output: MutableVec) { 203 if (rhs == 0f) { 204 throw IllegalArgumentException("Cannot divide by zero") 205 } 206 output.x = lhs.x / rhs 207 output.y = lhs.y / rhs 208 } 209 210 /** 211 * Returns the dot product (⋅) of the two vectors. The dot product has the property that, 212 * for vectors a and b: a ⋅ b = ‖a‖ * ‖b‖ * cos(θ) where ‖d‖ is the magnitude of the vector, 213 * and θ is the angle from a to b. 214 */ 215 @JvmStatic public fun dotProduct(lhs: Vec, rhs: Vec): Float = lhs.x * rhs.x + lhs.y * rhs.y 216 217 /** 218 * Returns the determinant (×) of the two vectors. The determinant can be thought of as the 219 * z-component of the 3D cross product of the two vectors, if they were placed on the 220 * xy-plane in 3D space. The determinant has the property that, for vectors a and b: a × b = 221 * ‖a‖ * ‖b‖ * sin(θ) where ‖d‖ is the magnitude of the vector, and θ is the signed angle 222 * from a to b. 223 */ 224 @JvmStatic 225 public fun determinant(lhs: Vec, rhs: Vec): Float { 226 return lhs.x * rhs.y - lhs.y * rhs.x 227 } 228 229 /** 230 * Returns the absolute angle between the given vectors. The return value will lie in the 231 * interval [0, π]. 232 */ 233 @AngleRadiansFloat 234 @FloatRange(from = 0.0, to = Math.PI) 235 @JvmStatic 236 public fun absoluteAngleBetween(lhs: Vec, rhs: Vec): Float { 237 return VecNative.absoluteAngleBetween(lhs.x, lhs.y, rhs.x, rhs.y) 238 } 239 240 /** 241 * Returns the signed angle between the given vectors. The return value will lie in the 242 * interval (-π, π]. 243 */ 244 @AngleRadiansFloat 245 @FloatRange(from = -Math.PI, to = Math.PI, fromInclusive = false) 246 @JvmStatic 247 public fun signedAngleBetween(lhs: Vec, rhs: Vec): Float { 248 return VecNative.signedAngleBetween(lhs.x, lhs.y, rhs.x, rhs.y) 249 } 250 251 /** 252 * Returns true if [first] and [second] have the same values for all properties of [Vec]. 253 */ 254 internal fun areEquivalent(first: Vec, second: Vec): Boolean { 255 return first.x == second.x && first.y == second.y 256 } 257 258 /** Returns a hash code for [vec] using its [Vec] properties. */ 259 internal fun hash(vec: Vec) = 31 * vec.x.hashCode() + vec.y.hashCode() 260 261 /** Returns a string representation for [vec] using its [Vec] properties. */ 262 internal fun string(vec: Vec): String = "Vec(x=${vec.x}, y=${vec.y})" 263 } 264 } 265