vector2d 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +1 -1
  3. data/.gitignore +3 -1
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +1 -1
  6. data/.yardopts +11 -0
  7. data/CHANGELOG.md +216 -0
  8. data/Gemfile +5 -1
  9. data/Gemfile.lock +35 -5
  10. data/README.md +325 -12
  11. data/Rakefile +18 -0
  12. data/benchmark/vector_comparison.rb +129 -0
  13. data/lib/vector2d/angles.rb +315 -0
  14. data/lib/vector2d/arithmetic.rb +97 -0
  15. data/lib/vector2d/comparison.rb +188 -0
  16. data/lib/vector2d/componentwise.rb +239 -0
  17. data/lib/vector2d/constructors.rb +137 -0
  18. data/lib/vector2d/conversions.rb +139 -0
  19. data/lib/vector2d/coordinates.rb +22 -0
  20. data/lib/vector2d/deprecation.rb +16 -0
  21. data/lib/vector2d/dimensions.rb +297 -0
  22. data/lib/vector2d/interpolation.rb +191 -0
  23. data/lib/vector2d/lengths.rb +284 -0
  24. data/lib/vector2d/matrix_interop.rb +93 -0
  25. data/lib/vector2d/parsing.rb +142 -0
  26. data/lib/vector2d/projection.rb +208 -0
  27. data/lib/vector2d/version.rb +2 -1
  28. data/lib/vector2d.rb +172 -60
  29. data/spec/lib/vector2d/angles_spec.rb +441 -0
  30. data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
  31. data/spec/lib/vector2d/comparison_spec.rb +358 -0
  32. data/spec/lib/vector2d/componentwise_spec.rb +300 -0
  33. data/spec/lib/vector2d/constructors_spec.rb +299 -0
  34. data/spec/lib/vector2d/conversions_spec.rb +234 -0
  35. data/spec/lib/vector2d/dimensions_spec.rb +820 -0
  36. data/spec/lib/vector2d/interpolation_spec.rb +322 -0
  37. data/spec/lib/vector2d/lengths_spec.rb +457 -0
  38. data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
  39. data/spec/lib/vector2d/parsing_spec.rb +329 -0
  40. data/spec/lib/vector2d/projection_spec.rb +306 -0
  41. data/spec/lib/vector2d_documentation_spec.rb +38 -0
  42. data/spec/lib/vector2d_immutability_spec.rb +263 -0
  43. data/spec/lib/vector2d_spec.rb +90 -50
  44. data/spec/lib/vector2d_subclassing_spec.rb +210 -0
  45. data/spec/lib/vector2d_tags_spec.rb +43 -0
  46. data/spec/spec_helper.rb +2 -0
  47. data/spec/support/doc_examples/comments.rb +65 -0
  48. data/spec/support/doc_examples/markdown.rb +50 -0
  49. data/spec/support/doc_examples.rb +94 -0
  50. data/spec/support/doc_tags.rb +151 -0
  51. data/spec/support/shared_examples/class_preserving_method.rb +10 -0
  52. data/spec/support/shared_examples/deprecated_method.rb +25 -0
  53. data/spec/support/shared_examples/parsed_vector.rb +11 -0
  54. data/vector2d.gemspec +2 -1
  55. metadata +42 -13
  56. data/.travis.yml +0 -10
  57. data/lib/vector2d/calculations.rb +0 -141
  58. data/lib/vector2d/coercions.rb +0 -64
  59. data/lib/vector2d/fitting.rb +0 -60
  60. data/lib/vector2d/properties.rb +0 -46
  61. data/lib/vector2d/transformations.rb +0 -98
  62. data/spec/lib/vector2d/coercions_spec.rb +0 -59
  63. data/spec/lib/vector2d/fitting_spec.rb +0 -70
  64. data/spec/lib/vector2d/properties_spec.rb +0 -47
  65. data/spec/lib/vector2d/transformations_spec.rb +0 -139
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Vector2d
4
+ # The angle API. Radians are the native unit: every angle is measured
5
+ # counterclockwise from the positive x axis, and every method takes
6
+ # and returns radians unless its name says degrees.
7
+ #
8
+ # The degree methods are conversions layered on top, not a second
9
+ # angle API. Two general converters, .radians and .degrees, do the
10
+ # arithmetic once. Three degree variants build on them, each one the
11
+ # radian method with the angle converted: .from_degrees is
12
+ # .from_angle, #angle_in_degrees is #angle, and #rotate_degrees is
13
+ # #rotate. The rest of the angle API stays radians only. Convert at
14
+ # the call site for those.
15
+ #
16
+ # Vector2d(2, 3).rotate_around(Vector2d(1, 1), Vector2d.radians(90))
17
+ # # => Vector2d(-1.0,2.0)
18
+ #
19
+ # Vector2d.degrees(Vector2d(2, 3).angle_to(Vector2d(4, 5)))
20
+ # # => -4.9697..
21
+ #
22
+ # Porting note: pygame's rotate() and Unity's Vector2.Angle are in
23
+ # degrees, so those angles need .radians on the way in. Godot and
24
+ # Rust's glam are radians like this library, and their angles carry
25
+ # over unconverted.
26
+ module Angles
27
+ # The angle methods taking both vectors as arguments, and the
28
+ # conversions between radians and degrees. Extended into Vector2d,
29
+ # so they are called on the class.
30
+ #
31
+ # Vector2d.angle_between(Vector2d(2, 3), Vector2d(4, 5))
32
+ # # => 0.0867..
33
+ #
34
+ module ClassMethods
35
+ # Calculates the signed angle in radians from the first vector to
36
+ # the second, in the range -PI..PI. The angle is positive when the
37
+ # second vector is counterclockwise from the first, and reversing
38
+ # the arguments flips its sign.
39
+ #
40
+ # v1 = Vector2d(2, 3)
41
+ # v2 = Vector2d(4, 5)
42
+ # Vector2d.angle_to(v1, v2) # => -0.0867..
43
+ # Vector2d.angle_to(v2, v1) # => 0.0867..
44
+ #
45
+ # Only the directions matter, not the magnitudes. The zero vector
46
+ # has no direction, and the angle to or from it is zero.
47
+ #
48
+ # Vector2d.angle_to(v1, Vector2d(0, 0)) # => 0.0
49
+ #
50
+ # @param vector1 [Vector2d] the vector the angle is measured from
51
+ # @param vector2 [Vector2d] the vector the angle is measured to
52
+ # @return [Float] the angle in radians, in -PI..PI
53
+ def angle_to(vector1, vector2)
54
+ Math.atan2(cross_product(vector1, vector2),
55
+ dot_product(vector1, vector2))
56
+ end
57
+
58
+ # Calculates the unsigned angle between two vectors in radians, in
59
+ # the range 0..PI. This is the magnitude of .angle_to, so the
60
+ # order of the arguments does not matter.
61
+ #
62
+ # v1 = Vector2d(2, 3)
63
+ # v2 = Vector2d(4, 5)
64
+ # Vector2d.angle_between(v1, v2) # => 0.0867..
65
+ # Vector2d.angle_between(v2, v1) # => 0.0867..
66
+ #
67
+ # Only the directions matter, not the magnitudes. The zero vector
68
+ # has no direction, and the angle between it and anything is zero.
69
+ #
70
+ # Vector2d.angle_between(v1, Vector2d(0, 0)) # => 0.0
71
+ #
72
+ # @param vector1 [Vector2d] one of the vectors
73
+ # @param vector2 [Vector2d] the other vector
74
+ # @return [Float] the angle in radians, in 0..PI
75
+ def angle_between(vector1, vector2)
76
+ angle_to(vector1, vector2).abs
77
+ end
78
+
79
+ # Converts an angle from degrees to radians, the unit the rest of
80
+ # the library speaks. The result is always a float.
81
+ #
82
+ # Vector2d.radians(0) # => 0.0
83
+ # Vector2d.radians(90) # => 1.5707..
84
+ # Vector2d.radians(180) # => 3.1415..
85
+ #
86
+ # .degrees converts back.
87
+ #
88
+ # Vector2d.degrees(Vector2d.radians(90)) # => 90.0
89
+ #
90
+ # Raises ArgumentError unless the angle is a real number.
91
+ # Complex numbers are not angles, and are rejected.
92
+ #
93
+ # Vector2d.radians(Complex(1, 2)) # => ArgumentError
94
+ #
95
+ # @param degrees [Integer, Float, Rational, BigDecimal]
96
+ # the angle in degrees
97
+ # @return [Float] the angle in radians
98
+ def radians(degrees)
99
+ coordinate(degrees).to_f * Math::PI / 180
100
+ end
101
+
102
+ # Converts an angle from radians to degrees, the inverse of
103
+ # .radians. The result is always a float.
104
+ #
105
+ # Vector2d.degrees(0) # => 0.0
106
+ # Vector2d.degrees(Math::PI / 2) # => 90.0
107
+ # Vector2d.degrees(Math::PI) # => 180.0
108
+ #
109
+ # This is the conversion #angle_in_degrees applies to #angle, and
110
+ # the one to reach for with the angle methods that have no degree
111
+ # variant.
112
+ #
113
+ # Vector2d.degrees(Vector2d(0, 1).angle) # => 90.0
114
+ #
115
+ # Raises ArgumentError unless the angle is a real number.
116
+ # Complex numbers are not angles, and are rejected.
117
+ #
118
+ # Vector2d.degrees(Complex(1, 2)) # => ArgumentError
119
+ #
120
+ # @param radians [Integer, Float, Rational, BigDecimal]
121
+ # the angle in radians
122
+ # @return [Float] the angle in degrees
123
+ def degrees(radians)
124
+ coordinate(radians).to_f * 180 / Math::PI
125
+ end
126
+
127
+ # Creates a vector from an angle in degrees, with an optional
128
+ # length. This is .from_angle with the angle put through
129
+ # .radians, and is the same in every other respect: angles are
130
+ # measured counterclockwise from the positive x axis, and
131
+ # coordinates are always floats.
132
+ #
133
+ # Vector2d.from_degrees(0) # => Vector2d(1.0,0.0)
134
+ # Vector2d.from_degrees(45) # => Vector2d(0.7071..,0.7071..)
135
+ # Vector2d.from_degrees(45, 2.0) # => Vector2d(1.4142..,1.4142..)
136
+ #
137
+ # #angle_in_degrees takes the angle back.
138
+ #
139
+ # Vector2d.from_degrees(90).angle_in_degrees # => 90.0
140
+ #
141
+ # Raises ArgumentError unless both arguments are real numbers.
142
+ #
143
+ # Vector2d.from_degrees(Complex(1, 2)) # => ArgumentError
144
+ #
145
+ # @param angle [Integer, Float, Rational, BigDecimal] the angle in degrees
146
+ # @param length [Integer, Float, Rational, BigDecimal]
147
+ # the length of the vector
148
+ # @return [Vector2d] a vector of the receiver's class
149
+ def from_degrees(angle, length = 1.0)
150
+ from_angle(radians(angle), length)
151
+ end
152
+ end
153
+
154
+ # Angle of vector.
155
+ #
156
+ # Vector2d(2, 3).angle # => 0.9827..
157
+ #
158
+ # @return [Float] the angle in radians
159
+ def angle
160
+ Math.atan2(y, x)
161
+ end
162
+
163
+ # Signed angle in radians from this vector to another vector, in the
164
+ # range -PI..PI. The angle is positive when the other vector is
165
+ # counterclockwise from this one.
166
+ #
167
+ # v1 = Vector2d(2, 3)
168
+ # v2 = Vector2d(4, 5)
169
+ # v1.angle_to(v2) # => -0.0867..
170
+ # v2.angle_to(v1) # => 0.0867..
171
+ #
172
+ # Only the directions matter, not the magnitudes. The zero vector
173
+ # has no direction, and the angle to or from it is zero.
174
+ #
175
+ # v1.angle_to(Vector2d(0, 0)) # => 0.0
176
+ #
177
+ # @!macro coercible
178
+ # @return [Float] the angle in radians, in -PI..PI
179
+ def angle_to(other)
180
+ v = coerce_vector(other)
181
+ self.class.angle_to(self, v)
182
+ end
183
+
184
+ # Unsigned angle in radians between this vector and another vector,
185
+ # in the range 0..PI. This is the magnitude of #angle_to, so it is
186
+ # the same in either direction.
187
+ #
188
+ # v1 = Vector2d(2, 3)
189
+ # v2 = Vector2d(4, 5)
190
+ # v1.angle_between(v2) # => 0.0867..
191
+ # v2.angle_between(v1) # => 0.0867..
192
+ #
193
+ # Only the directions matter, not the magnitudes. The zero vector
194
+ # has no direction, and the angle between it and anything is zero.
195
+ #
196
+ # v1.angle_between(Vector2d(0, 0)) # => 0.0
197
+ #
198
+ # @!macro coercible
199
+ # @return [Float] the angle in radians, in 0..PI
200
+ def angle_between(other)
201
+ angle_to(other).abs
202
+ end
203
+ alias angle_with angle_between
204
+
205
+ # Polar coordinates of vector, as a [length, angle] array.
206
+ #
207
+ # Vector2d(2, 3).to_polar # => [3.6055.., 0.9827..]
208
+ #
209
+ # Vector2d.from_angle takes the same pair back.
210
+ #
211
+ # length, angle = Vector2d(2, 3).to_polar
212
+ # Vector2d.from_angle(angle, length) # => Vector2d(2.0,3.0)
213
+ #
214
+ # @return [Array(Float, Float)] the length and the angle in radians
215
+ def to_polar
216
+ [length, angle]
217
+ end
218
+
219
+ # Rotates the vector around the origin. The angle is in radians, and
220
+ # a positive angle turns counterclockwise.
221
+ #
222
+ # Vector2d(2, 3).rotate(Math::PI / 2) # => Vector2d(-3.0,2.0)
223
+ #
224
+ # Raises ArgumentError unless the angle is a real number.
225
+ #
226
+ # Vector2d(2, 3).rotate(Complex(1, 2)) # => ArgumentError
227
+ #
228
+ # @param angle [Integer, Float, Rational, BigDecimal] the angle in radians
229
+ # @return [self]
230
+ def rotate(angle)
231
+ angle = coordinate(angle)
232
+ cos = Math.cos(angle)
233
+ sin = Math.sin(angle)
234
+ build((x * cos) - (y * sin), (x * sin) + (y * cos))
235
+ end
236
+
237
+ # Rotates the vector around another point. The center is coerced, so
238
+ # scalars work too. The angle is in radians, and a positive angle
239
+ # turns counterclockwise.
240
+ #
241
+ # Vector2d(2, 1).rotate_around(Vector2d(1, 1), Math::PI / 2)
242
+ # # => Vector2d(1.0,2.0)
243
+ #
244
+ # @!macro coercible
245
+ # @param angle [Integer, Float, Rational, BigDecimal] the angle in radians
246
+ # @return [self]
247
+ def rotate_around(center, angle)
248
+ center_v = coerce_vector(center)
249
+ (self - center_v).rotate(angle) + center_v
250
+ end
251
+
252
+ # Returns the vector rotated a quarter turn counterclockwise.
253
+ #
254
+ # Vector2d(2, 3).perpendicular # => Vector2d(-3,2)
255
+ #
256
+ # Counterclockwise is the same positive direction #rotate turns in.
257
+ # Use #perpendicular_cw for the other one.
258
+ #
259
+ # @return [self]
260
+ def perpendicular
261
+ build(-y, x)
262
+ end
263
+
264
+ # Returns the vector rotated a quarter turn clockwise.
265
+ #
266
+ # Vector2d(2, 3).perpendicular_cw # => Vector2d(3,-2)
267
+ #
268
+ # @return [self]
269
+ def perpendicular_cw
270
+ build(y, -x)
271
+ end
272
+
273
+ # Angle of the vector in degrees. This is #angle put through
274
+ # .degrees, and follows the same convention: angles are measured
275
+ # counterclockwise from the positive x axis, in the range
276
+ # -180..180.
277
+ #
278
+ # Vector2d(1, 0).angle_in_degrees # => 0.0
279
+ # Vector2d(0, 1).angle_in_degrees # => 90.0
280
+ # Vector2d(2, 3).angle_in_degrees # => 56.3099..
281
+ # Vector2d(0, -1).angle_in_degrees # => -90.0
282
+ #
283
+ # Vector2d.from_degrees takes the angle back.
284
+ #
285
+ # Vector2d.from_degrees(Vector2d(2, 3).angle_in_degrees, 5.0)
286
+ # # => Vector2d(2.7735..,4.1602..)
287
+ #
288
+ # @return [Float] the angle in degrees, in -180..180
289
+ def angle_in_degrees
290
+ self.class.degrees(angle)
291
+ end
292
+
293
+ # Rotates the vector around the origin by an angle in degrees. This
294
+ # is #rotate with the angle put through .radians, and turns the
295
+ # same way: a positive angle turns counterclockwise.
296
+ #
297
+ # Vector2d(2, 3).rotate_degrees(90) # => Vector2d(-3.0,2.0)
298
+ #
299
+ # The two agree exactly wherever .radians lands on the angle
300
+ # #rotate would have been given.
301
+ #
302
+ # Vector2d(2, 3).rotate_degrees(90) == Vector2d(2, 3).rotate(Math::PI / 2)
303
+ # # => true
304
+ #
305
+ # Raises ArgumentError unless the angle is a real number.
306
+ #
307
+ # Vector2d(2, 3).rotate_degrees(Complex(1, 2)) # => ArgumentError
308
+ #
309
+ # @param angle [Integer, Float, Rational, BigDecimal] the angle in degrees
310
+ # @return [self]
311
+ def rotate_degrees(angle)
312
+ rotate(self.class.radians(angle))
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Vector2d
4
+ # The operators. A scalar operand applies to both coordinates.
5
+ # Anything else is coerced into a vector and applied one axis at a
6
+ # time.
7
+ module Arithmetic
8
+ # Multiplies vectors.
9
+ #
10
+ # Vector2d(1, 2) * Vector2d(2, 3) # => Vector2d(2, 6)
11
+ # Vector2d(1, 2) * 2 # => Vector2d(2, 4)
12
+ #
13
+ # @!macro coercible
14
+ # @return [self]
15
+ def *(other)
16
+ calculate_each(:*, other)
17
+ end
18
+
19
+ # Divides vectors.
20
+ #
21
+ # Vector2d(4, 2) / Vector2d(2, 1) # => Vector2d(2, 2)
22
+ # Vector2d(4, 2) / 2 # => Vector2d(2, 1)
23
+ #
24
+ # @!macro coercible
25
+ # @return [self]
26
+ def /(other)
27
+ calculate_each(:/, other)
28
+ end
29
+
30
+ # Adds vectors.
31
+ #
32
+ # Vector2d(1, 2) + Vector2d(2, 3) # => Vector2d(3, 5)
33
+ # Vector2d(1, 2) + 2 # => Vector2d(3, 4)
34
+ #
35
+ # @!macro coercible
36
+ # @return [self]
37
+ def +(other)
38
+ calculate_each(:+, other)
39
+ end
40
+
41
+ # Subtracts vectors.
42
+ #
43
+ # Vector2d(2, 3) - Vector2d(2, 1) # => Vector2d(0, 2)
44
+ # Vector2d(4, 3) - 1 # => Vector2d(3, 2)
45
+ #
46
+ # @!macro coercible
47
+ # @return [self]
48
+ def -(other)
49
+ calculate_each(:-, other)
50
+ end
51
+
52
+ # Returns the vector reversed.
53
+ #
54
+ # -Vector2d(2, 3) # => Vector2d(-2,-3)
55
+ #
56
+ # @return [self]
57
+ def -@
58
+ reverse
59
+ end
60
+
61
+ # Returns the vector unchanged.
62
+ #
63
+ # +Vector2d(2, 3) # => Vector2d(2,3)
64
+ #
65
+ # @return [self]
66
+ def +@
67
+ self
68
+ end
69
+
70
+ # Reverses the vector.
71
+ #
72
+ # Vector2d(2, 3).reverse # => Vector2d(-2,-3)
73
+ #
74
+ # @return [self]
75
+ def reverse
76
+ build(-x, -y)
77
+ end
78
+
79
+ private
80
+
81
+ # Is the value a number both coordinates can be combined with
82
+ # directly? Complex is Numeric, but it is not a coordinate.
83
+ def real_number?(value)
84
+ value.is_a?(Numeric) && value.real?
85
+ end
86
+
87
+ def calculate_each(method, other)
88
+ return build(x.send(method, other), y.send(method, other)) if real_number?(other)
89
+
90
+ v = coerce_vector(other)
91
+ build(
92
+ x.send(method, v.x),
93
+ y.send(method, v.y)
94
+ )
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Vector2d
4
+ # Predicates relating two vectors, and the checks on coordinates that
5
+ # arithmetic can take out of range. Where #== compares exactly, the
6
+ # predicates here allow for floating point drift.
7
+ module Comparison
8
+ # Are the two vectors equal, give or take floating point drift?
9
+ # Arithmetic that should land on a given vector usually lands a few
10
+ # ulps off it instead, which #== reports as a difference.
11
+ #
12
+ # v1 = Vector2d(0.1, 0.2) + Vector2d(0.2, 0.4)
13
+ # v2 = Vector2d(0.3, 0.6)
14
+ # v1 # => Vector2d(0.30000000000000004,0.6000..)
15
+ # v1 == v2 # => false
16
+ # v1.approx_equal?(v2) # => true
17
+ #
18
+ # The other vector is coerced, unlike in #==, so anything .parse
19
+ # accepts is compared as a vector.
20
+ #
21
+ # v1.approx_equal?([0.3, 0.6]) # => true
22
+ # v1 == [0.3, 0.6] # => false
23
+ #
24
+ # The default tolerance is the one #parallel? and #perpendicular?
25
+ # use, a few ulps scaled by the magnitude of the vectors, so the
26
+ # same amount of drift is absorbed whatever the coordinates are
27
+ # sized like. It covers rounding error, and nothing more.
28
+ #
29
+ # big = Vector2d(1e8, 2e8)
30
+ # big.approx_equal?(big.rotate(2 * Math::PI)) # => true
31
+ # big.approx_equal?(Vector2d(1e8, 2.1e8)) # => false
32
+ #
33
+ # Pass a tolerance for anything looser. It is an absolute distance
34
+ # between the two vectors, and is not scaled.
35
+ #
36
+ # v3 = Vector2d(2, 3)
37
+ # v3.approx_equal?(Vector2d(2, 4), 1.5) # => true
38
+ # v3.approx_equal?(Vector2d(2, 4), 0.5) # => false
39
+ #
40
+ # Note that this is not a replacement for #==. Approximate equality
41
+ # is not transitive, and vectors that are approximately equal do not
42
+ # have the same #hash.
43
+ #
44
+ # @!macro coercible
45
+ # @param tolerance [Integer, Float, Rational, BigDecimal, nil]
46
+ # an absolute distance, or nil for the default scaled tolerance
47
+ # @return [Boolean]
48
+ def approx_equal?(other, tolerance = nil)
49
+ v = coerce_vector(other)
50
+ return distance(v) <= coordinate(tolerance) unless tolerance.nil?
51
+
52
+ near_zero?(distance(v), [1.0, length, v.length].max)
53
+ end
54
+
55
+ # Are the two vectors parallel? Vectors pointing in opposite
56
+ # directions are parallel too.
57
+ #
58
+ # v = Vector2d(2, 3)
59
+ # v.parallel?(Vector2d(4, 6)) # => true
60
+ # v.parallel?(Vector2d(-4, -6)) # => true
61
+ # v.parallel?(Vector2d(3, 2)) # => false
62
+ #
63
+ # Only the directions matter, not the magnitudes. The zero vector
64
+ # has no direction, and is parallel to everything.
65
+ #
66
+ # v.parallel?(Vector2d(0, 0)) # => true
67
+ #
68
+ # @!macro coercible
69
+ # @return [Boolean]
70
+ def parallel?(other)
71
+ v = coerce_vector(other)
72
+ return true if zero? || v.zero?
73
+
74
+ near_zero?(cross_product(v), length * v.length)
75
+ end
76
+
77
+ # Are the two vectors linearly independent? This is the inverse of
78
+ # #parallel?, and matches Vector#independent? in the standard
79
+ # library.
80
+ #
81
+ # v = Vector2d(2, 3)
82
+ # v.independent?(Vector2d(3, 2)) # => true
83
+ # v.independent?(Vector2d(4, 6)) # => false
84
+ # v.independent?(Vector2d(-4, -6)) # => false
85
+ #
86
+ # The zero vector is parallel to everything, so nothing is
87
+ # independent of it.
88
+ #
89
+ # v.independent?(Vector2d(0, 0)) # => false
90
+ #
91
+ # @!macro coercible
92
+ # @return [Boolean]
93
+ def independent?(other)
94
+ !parallel?(other)
95
+ end
96
+
97
+ # Do the two vectors point in opposite directions? Only the
98
+ # directions matter, not the magnitudes.
99
+ #
100
+ # v = Vector2d(2, 3)
101
+ # v.opposite?(Vector2d(-2, -3)) # => true
102
+ # v.opposite?(Vector2d(-4, -6)) # => true
103
+ # v.opposite?(Vector2d(4, 6)) # => false
104
+ # v.opposite?(Vector2d(3, 2)) # => false
105
+ #
106
+ # Opposite vectors are parallel, but #parallel? does not care which
107
+ # way along the line the other vector points.
108
+ #
109
+ # v.parallel?(Vector2d(-4, -6)) # => true
110
+ #
111
+ # The zero vector has no direction to be the opposite of, so unlike
112
+ # #parallel? and #perpendicular?, which it satisfies trivially,
113
+ # it is opposite to nothing.
114
+ #
115
+ # v.opposite?(Vector2d(0, 0)) # => false
116
+ #
117
+ # @!macro coercible
118
+ # @return [Boolean]
119
+ def opposite?(other)
120
+ v = coerce_vector(other)
121
+ return false if zero? || v.zero?
122
+
123
+ parallel?(v) && dot_product(v).negative?
124
+ end
125
+
126
+ # Are the two vectors perpendicular to each other?
127
+ #
128
+ # v = Vector2d(2, 3)
129
+ # v.perpendicular?(Vector2d(-3, 2)) # => true
130
+ # v.perpendicular?(Vector2d(3, 2)) # => false
131
+ #
132
+ # Only the directions matter, not the magnitudes. The zero vector
133
+ # has no direction, and is perpendicular to everything.
134
+ #
135
+ # v.perpendicular?(Vector2d(0, 0)) # => true
136
+ #
137
+ # @!macro coercible
138
+ # @return [Boolean]
139
+ def perpendicular?(other)
140
+ v = coerce_vector(other)
141
+ return true if zero? || v.zero?
142
+
143
+ near_zero?(dot_product(v), length * v.length)
144
+ end
145
+
146
+ # Are both coordinates finite?
147
+ #
148
+ # Vector2d(2, 3).finite? # => true
149
+ # Vector2d(2, Float::INFINITY).finite? # => false
150
+ # Vector2d(2, Float::NAN).finite? # => false
151
+ #
152
+ # @return [Boolean]
153
+ def finite?
154
+ x.finite? && y.finite?
155
+ end
156
+
157
+ # Is either coordinate NaN? Nothing else in the library produces
158
+ # one, but arithmetic on infinities does.
159
+ #
160
+ # Vector2d(2, 3).nan? # => false
161
+ # Vector2d(2, Float::NAN).nan? # => true
162
+ #
163
+ # (Vector2d(2, 3) * Float::INFINITY * 0).nan? # => true
164
+ #
165
+ # @return [Boolean]
166
+ def nan?
167
+ coordinate_nan?(x) || coordinate_nan?(y)
168
+ end
169
+
170
+ private
171
+
172
+ # Is the coordinate NaN? Only floats and decimals can be, the rest
173
+ # of Numeric has no answer to give.
174
+ def coordinate_nan?(value)
175
+ value.respond_to?(:nan?) && value.nan?
176
+ end
177
+
178
+ # Is a value close enough to zero to count as zero? The tolerance
179
+ # scales with the magnitudes the value was calculated from.
180
+ #
181
+ # near_zero?(1e-14) # => false
182
+ # near_zero?(1e-14, 1e6) # => true
183
+ #
184
+ def near_zero?(value, scale = 1.0)
185
+ value.abs < (4 * Float::EPSILON * scale)
186
+ end
187
+ end
188
+ end