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