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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +1 -1
- data/.yardopts +11 -0
- data/CHANGELOG.md +216 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +35 -5
- data/README.md +325 -12
- data/Rakefile +18 -0
- data/benchmark/vector_comparison.rb +129 -0
- data/lib/vector2d/angles.rb +315 -0
- data/lib/vector2d/arithmetic.rb +97 -0
- data/lib/vector2d/comparison.rb +188 -0
- data/lib/vector2d/componentwise.rb +239 -0
- data/lib/vector2d/constructors.rb +137 -0
- data/lib/vector2d/conversions.rb +139 -0
- data/lib/vector2d/coordinates.rb +22 -0
- data/lib/vector2d/deprecation.rb +16 -0
- data/lib/vector2d/dimensions.rb +297 -0
- data/lib/vector2d/interpolation.rb +191 -0
- data/lib/vector2d/lengths.rb +284 -0
- data/lib/vector2d/matrix_interop.rb +93 -0
- data/lib/vector2d/parsing.rb +142 -0
- data/lib/vector2d/projection.rb +208 -0
- data/lib/vector2d/version.rb +2 -1
- data/lib/vector2d.rb +172 -60
- data/spec/lib/vector2d/angles_spec.rb +441 -0
- data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
- data/spec/lib/vector2d/comparison_spec.rb +358 -0
- data/spec/lib/vector2d/componentwise_spec.rb +300 -0
- data/spec/lib/vector2d/constructors_spec.rb +299 -0
- data/spec/lib/vector2d/conversions_spec.rb +234 -0
- data/spec/lib/vector2d/dimensions_spec.rb +820 -0
- data/spec/lib/vector2d/interpolation_spec.rb +322 -0
- data/spec/lib/vector2d/lengths_spec.rb +457 -0
- data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
- data/spec/lib/vector2d/parsing_spec.rb +329 -0
- data/spec/lib/vector2d/projection_spec.rb +306 -0
- data/spec/lib/vector2d_documentation_spec.rb +38 -0
- data/spec/lib/vector2d_immutability_spec.rb +263 -0
- data/spec/lib/vector2d_spec.rb +90 -50
- data/spec/lib/vector2d_subclassing_spec.rb +210 -0
- data/spec/lib/vector2d_tags_spec.rb +43 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/doc_examples/comments.rb +65 -0
- data/spec/support/doc_examples/markdown.rb +50 -0
- data/spec/support/doc_examples.rb +94 -0
- data/spec/support/doc_tags.rb +151 -0
- data/spec/support/shared_examples/class_preserving_method.rb +10 -0
- data/spec/support/shared_examples/deprecated_method.rb +25 -0
- data/spec/support/shared_examples/parsed_vector.rb +11 -0
- data/vector2d.gemspec +2 -1
- metadata +42 -13
- data/.travis.yml +0 -10
- data/lib/vector2d/calculations.rb +0 -141
- data/lib/vector2d/coercions.rb +0 -64
- data/lib/vector2d/fitting.rb +0 -60
- data/lib/vector2d/properties.rb +0 -46
- data/lib/vector2d/transformations.rb +0 -98
- data/spec/lib/vector2d/coercions_spec.rb +0 -59
- data/spec/lib/vector2d/fitting_spec.rb +0 -70
- data/spec/lib/vector2d/properties_spec.rb +0 -47
- 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
|