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,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# The length of a vector, the distance between two, and the
|
|
5
|
+
# operations that change the length while keeping the direction.
|
|
6
|
+
module Lengths
|
|
7
|
+
# Length of vector.
|
|
8
|
+
#
|
|
9
|
+
# Vector2d(2, 3).length # => 3.6055..
|
|
10
|
+
#
|
|
11
|
+
# @return [Float]
|
|
12
|
+
def length
|
|
13
|
+
Math.sqrt(length_squared)
|
|
14
|
+
end
|
|
15
|
+
alias magnitude length
|
|
16
|
+
alias norm length
|
|
17
|
+
|
|
18
|
+
# Squared length of vector. Avoids the square root when lengths are
|
|
19
|
+
# only being compared to each other.
|
|
20
|
+
#
|
|
21
|
+
# Vector2d(2, 3).length_squared # => 13
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
24
|
+
def length_squared
|
|
25
|
+
(x * x) + (y * y)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @deprecated Use #length_squared instead.
|
|
29
|
+
#
|
|
30
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
31
|
+
def squared_length
|
|
32
|
+
warn_deprecated("Vector2d#squared_length is deprecated. Use #length_squared instead.")
|
|
33
|
+
length_squared
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Is this the zero vector?
|
|
37
|
+
#
|
|
38
|
+
# Vector2d(0, 0).zero? # => true
|
|
39
|
+
# Vector2d(2, 3).zero? # => false
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def zero?
|
|
43
|
+
length_squared.zero?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Is this the zero vector, give or take floating point drift? See
|
|
47
|
+
# #approx_equal? for what the tolerance is.
|
|
48
|
+
#
|
|
49
|
+
# Vector2d(0, 0).approx_zero? # => true
|
|
50
|
+
# Vector2d(1e-17, 0).approx_zero? # => true
|
|
51
|
+
# Vector2d(1e-15, 0).approx_zero? # => false
|
|
52
|
+
#
|
|
53
|
+
# An explicit tolerance is an absolute length.
|
|
54
|
+
#
|
|
55
|
+
# Vector2d(0.2, 0).approx_zero?(0.5) # => true
|
|
56
|
+
#
|
|
57
|
+
# @param tolerance [Integer, Float, Rational, BigDecimal, nil]
|
|
58
|
+
# an absolute length, or nil for the default scaled tolerance
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def approx_zero?(tolerance = nil)
|
|
61
|
+
return length <= coordinate(tolerance) unless tolerance.nil?
|
|
62
|
+
|
|
63
|
+
near_zero?(length)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Is this a normalized vector?
|
|
67
|
+
#
|
|
68
|
+
# Vector2d(0, 1).normalized? # => true
|
|
69
|
+
# Vector2d(2, 3).normalized? # => false
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def normalized?
|
|
73
|
+
near_zero?(length - 1.0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalizes the vector.
|
|
77
|
+
#
|
|
78
|
+
# vector = Vector2d(2, 3)
|
|
79
|
+
# vector.normalize # => Vector2d(0.5547.., 0.8320..)
|
|
80
|
+
# vector.normalize.length # => 1.0
|
|
81
|
+
#
|
|
82
|
+
# The zero vector has no direction, and is returned unchanged.
|
|
83
|
+
#
|
|
84
|
+
# Vector2d(0, 0).normalize # => Vector2d(0,0)
|
|
85
|
+
#
|
|
86
|
+
# @return [self]
|
|
87
|
+
def normalize
|
|
88
|
+
resize(1.0)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Changes magnitude of vector.
|
|
92
|
+
#
|
|
93
|
+
# Vector2d(2, 3).resize(1.0) # => Vector2d(0.5547.., 0.8320..)
|
|
94
|
+
#
|
|
95
|
+
# The zero vector has no direction, and is returned unchanged.
|
|
96
|
+
#
|
|
97
|
+
# Vector2d(0, 0).resize(1.0) # => Vector2d(0,0)
|
|
98
|
+
#
|
|
99
|
+
# A negative length reverses the vector.
|
|
100
|
+
#
|
|
101
|
+
# Vector2d(2, 3).resize(-1.0) # => Vector2d(-0.5547..,-0.8320..)
|
|
102
|
+
#
|
|
103
|
+
# @param new_length [Integer, Float, Rational, BigDecimal] the new length
|
|
104
|
+
# @return [self]
|
|
105
|
+
def resize(new_length)
|
|
106
|
+
new_length = coordinate(new_length)
|
|
107
|
+
return self if zero?
|
|
108
|
+
|
|
109
|
+
self * (new_length / length)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Limits the length of the vector, scaling it down if it is longer
|
|
113
|
+
# than max. The direction is kept.
|
|
114
|
+
#
|
|
115
|
+
# vector = Vector2d(2.0, 3.0)
|
|
116
|
+
# vector.limit_length(5.0) # => Vector2d(2.0, 3.0)
|
|
117
|
+
# vector.limit_length(1.0) # => Vector2d(0.5547.., 0.8320..)
|
|
118
|
+
#
|
|
119
|
+
# #clamp_length bounds the length at both ends.
|
|
120
|
+
#
|
|
121
|
+
# The zero vector has no direction, and is returned unchanged.
|
|
122
|
+
#
|
|
123
|
+
# Vector2d(0, 0).limit_length(1.0) # => Vector2d(0,0)
|
|
124
|
+
#
|
|
125
|
+
# Lengths can't be negative.
|
|
126
|
+
#
|
|
127
|
+
# Vector2d(2, 3).limit_length(-1.0) # => ArgumentError
|
|
128
|
+
#
|
|
129
|
+
# @param max [Integer, Float, Rational, BigDecimal] the maximum length
|
|
130
|
+
# @return [self]
|
|
131
|
+
def limit_length(max)
|
|
132
|
+
resize(length.clamp(..length_bound(max, "max")))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Clamps the length of the vector between a minimum and a maximum,
|
|
136
|
+
# scaling it up if it is shorter than min and down if it is longer
|
|
137
|
+
# than max. The direction is kept.
|
|
138
|
+
#
|
|
139
|
+
# vector = Vector2d(2.0, 3.0)
|
|
140
|
+
# vector.clamp_length(5.0, 10.0) # => Vector2d(2.7735.., 4.1602..)
|
|
141
|
+
# vector.clamp_length(1.0, 10.0) # => Vector2d(2.0, 3.0)
|
|
142
|
+
#
|
|
143
|
+
# The bounds can also be given as a single range, which may be
|
|
144
|
+
# beginless or endless to clamp only one side, as in #clamp.
|
|
145
|
+
#
|
|
146
|
+
# vector.clamp_length(5.0..10.0) # => Vector2d(2.7735.., 4.1602..)
|
|
147
|
+
# vector.clamp_length(..1.0) # => Vector2d(0.5547.., 0.8320..)
|
|
148
|
+
# vector.clamp_length(5.0..) # => Vector2d(2.7735.., 4.1602..)
|
|
149
|
+
#
|
|
150
|
+
# A maximum on its own is #limit_length.
|
|
151
|
+
#
|
|
152
|
+
# The range must not exclude its end, as with Comparable#clamp.
|
|
153
|
+
#
|
|
154
|
+
# vector.clamp_length(5.0...10.0) # => ArgumentError
|
|
155
|
+
#
|
|
156
|
+
# The zero vector has no direction, and is returned unchanged. It
|
|
157
|
+
# can't be scaled up to a minimum length.
|
|
158
|
+
#
|
|
159
|
+
# Vector2d(0, 0).clamp_length(1.0, 2.0) # => Vector2d(0,0)
|
|
160
|
+
#
|
|
161
|
+
# Lengths can't be negative, and min can't exceed max.
|
|
162
|
+
#
|
|
163
|
+
# Vector2d(2, 3).clamp_length(-1.0, 1.0) # => ArgumentError
|
|
164
|
+
# Vector2d(2, 3).clamp_length(4.0, 2.0) # => ArgumentError
|
|
165
|
+
#
|
|
166
|
+
# @overload clamp_length(min, max)
|
|
167
|
+
# @param min [Integer, Float, Rational, BigDecimal] the minimum length
|
|
168
|
+
# @param max [Integer, Float, Rational, BigDecimal] the maximum length
|
|
169
|
+
# @overload clamp_length(range)
|
|
170
|
+
# @param range [Range] both bounds, and may be beginless or endless
|
|
171
|
+
# @return [self]
|
|
172
|
+
def clamp_length(min, max = nil)
|
|
173
|
+
min_length, max_length = clamp_length_bounds(min, max)
|
|
174
|
+
resize(length.clamp(Range.new(min_length, max_length)))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Calculates the distance between two vectors.
|
|
178
|
+
#
|
|
179
|
+
# v1 = Vector2d(2, 3)
|
|
180
|
+
# v2 = Vector2d(3, 4)
|
|
181
|
+
# v1.distance(v2) # => 1.4142..
|
|
182
|
+
#
|
|
183
|
+
# @!macro coercible
|
|
184
|
+
# @return [Float]
|
|
185
|
+
def distance(other)
|
|
186
|
+
(self - other).length
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Calculate squared distance between vectors. Avoids the square root
|
|
190
|
+
# when distances are only being compared to each other.
|
|
191
|
+
#
|
|
192
|
+
# v1 = Vector2d(2, 3)
|
|
193
|
+
# v2 = Vector2d(5, 6)
|
|
194
|
+
# v1.distance_squared(v2) # => 18
|
|
195
|
+
#
|
|
196
|
+
# @!macro coercible
|
|
197
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
198
|
+
def distance_squared(other)
|
|
199
|
+
(self - other).length_squared
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @deprecated Use #distance_squared instead.
|
|
203
|
+
#
|
|
204
|
+
# @!macro coercible
|
|
205
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
206
|
+
def squared_distance(other)
|
|
207
|
+
warn_deprecated("Vector2d#squared_distance is deprecated. Use #distance_squared instead.")
|
|
208
|
+
distance_squared(other)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Calculates the Manhattan distance between two vectors, the sum of
|
|
212
|
+
# the absolute differences along each axis.
|
|
213
|
+
#
|
|
214
|
+
# v1 = Vector2d(2, 3)
|
|
215
|
+
# v2 = Vector2d(5, 7)
|
|
216
|
+
# v1.manhattan_distance(v2) # => 7
|
|
217
|
+
#
|
|
218
|
+
# @!macro coercible
|
|
219
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
220
|
+
def manhattan_distance(other)
|
|
221
|
+
(self - other).abs.to_a.sum
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Calculates the Chebyshev distance between two vectors, the largest
|
|
225
|
+
# absolute difference along any axis.
|
|
226
|
+
#
|
|
227
|
+
# v1 = Vector2d(2, 3)
|
|
228
|
+
# v2 = Vector2d(5, 7)
|
|
229
|
+
# v1.chebyshev_distance(v2) # => 4
|
|
230
|
+
#
|
|
231
|
+
# @!macro coercible
|
|
232
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
233
|
+
def chebyshev_distance(other)
|
|
234
|
+
(self - other).abs.to_a.max
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Unit vector pointing from this vector to another vector, the
|
|
238
|
+
# direction a step from here to there would take. The other vector
|
|
239
|
+
# is coerced, so scalars work too.
|
|
240
|
+
#
|
|
241
|
+
# v1 = Vector2d(2, 3)
|
|
242
|
+
# v1.direction_to(Vector2d(2, 6)) # => Vector2d(0.0,1.0)
|
|
243
|
+
# v1.direction_to(Vector2d(5, 7)) # => Vector2d(0.6000..,0.8)
|
|
244
|
+
#
|
|
245
|
+
# This is (other - self).normalize. #distance measures the same
|
|
246
|
+
# step, and #move_toward takes it.
|
|
247
|
+
#
|
|
248
|
+
# There is no direction to where you already are. The zero vector
|
|
249
|
+
# has no direction, and is returned.
|
|
250
|
+
#
|
|
251
|
+
# v1.direction_to(v1) # => Vector2d(0,0)
|
|
252
|
+
#
|
|
253
|
+
# @!macro coercible
|
|
254
|
+
# @return [self]
|
|
255
|
+
def direction_to(other)
|
|
256
|
+
v = coerce_vector(other)
|
|
257
|
+
build(v.x - x, v.y - y).normalize
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def clamp_length_bounds(min, max)
|
|
263
|
+
return range_length_bounds(min, max) if min.is_a?(Range)
|
|
264
|
+
|
|
265
|
+
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if max.nil?
|
|
266
|
+
|
|
267
|
+
[length_bound(min, "min"), length_bound(max, "max")]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def range_length_bounds(range, max)
|
|
271
|
+
range_bounds(range, max).zip(%w[min max]).map do |bound, name|
|
|
272
|
+
bound && length_bound(bound, name)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Validates one end of a length clamp. Lengths are never negative.
|
|
277
|
+
def length_bound(value, name)
|
|
278
|
+
value = coordinate(value)
|
|
279
|
+
raise ArgumentError, "negative #{name} length: #{value}" if value.negative?
|
|
280
|
+
|
|
281
|
+
value
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# Interoperability with the Matrix and Vector classes from the
|
|
5
|
+
# standard library.
|
|
6
|
+
#
|
|
7
|
+
# The matrix library is a bundled gem, and is only loaded when a
|
|
8
|
+
# conversion needs it. Nothing here loads it on its own, so it has to
|
|
9
|
+
# be in the Gemfile of applications using these methods.
|
|
10
|
+
module MatrixInterop
|
|
11
|
+
class << self
|
|
12
|
+
# Is the object a Matrix?
|
|
13
|
+
#
|
|
14
|
+
# @param object [Object] any object
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def matrix?(object)
|
|
17
|
+
defined?(::Matrix) && object.is_a?(::Matrix)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Is the object a Vector?
|
|
21
|
+
#
|
|
22
|
+
# @param object [Object] any object
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def vector?(object)
|
|
25
|
+
defined?(::Vector) && object.is_a?(::Vector)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Implements Ruby's coercion protocol for matrices, so a vector can
|
|
30
|
+
# be the right hand operand of a matrix. The result is a Vector, as
|
|
31
|
+
# the matrix is the receiver of the operation.
|
|
32
|
+
#
|
|
33
|
+
# Matrix[[0, -1], [1, 0]] * Vector2d(3, 4) # => Vector[-4, 3]
|
|
34
|
+
#
|
|
35
|
+
# Use #transform to get a Vector2d back.
|
|
36
|
+
#
|
|
37
|
+
# Vectors are coerced the other way around, as the two dimensional
|
|
38
|
+
# vector is the more specific type.
|
|
39
|
+
#
|
|
40
|
+
# Vector[1, 2] + Vector2d(3, 4) # => Vector2d(4,6)
|
|
41
|
+
#
|
|
42
|
+
# @!macro coercible
|
|
43
|
+
# @return [Array(::Matrix, ::Vector), Array(self, self)] the other
|
|
44
|
+
# operand and this vector, converted to a Vector when the other
|
|
45
|
+
# operand is a Matrix
|
|
46
|
+
def coerce(other)
|
|
47
|
+
return [other, to_vector] if MatrixInterop.matrix?(other)
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Converts vector to a 2x1 column Matrix from the standard library,
|
|
53
|
+
# the same shape Vector#to_matrix returns.
|
|
54
|
+
#
|
|
55
|
+
# Vector2d(2, 3).to_matrix # => Matrix[[2], [3]]
|
|
56
|
+
#
|
|
57
|
+
# @return [::Matrix] a 2x1 column matrix
|
|
58
|
+
def to_matrix
|
|
59
|
+
require "matrix"
|
|
60
|
+
::Matrix[[x], [y]]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Converts vector to a Vector from the standard library. This is
|
|
64
|
+
# the only to_* method that leaves the class behind, #to_f_vector
|
|
65
|
+
# and #to_i_vector return a vector of this class.
|
|
66
|
+
#
|
|
67
|
+
# Vector2d(2, 3).to_vector # => Vector[2, 3]
|
|
68
|
+
#
|
|
69
|
+
# @return [::Vector]
|
|
70
|
+
def to_vector
|
|
71
|
+
require "matrix"
|
|
72
|
+
::Vector[x, y]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Multiplies a 2x2 Matrix by this vector, treating the vector as a
|
|
76
|
+
# column. Unlike coercion, this keeps the class of the receiver.
|
|
77
|
+
#
|
|
78
|
+
# Vector2d(3, 4).transform(Matrix[[0, -1], [1, 0]])
|
|
79
|
+
# # => Vector2d(-4,3)
|
|
80
|
+
#
|
|
81
|
+
# Raises TypeError unless the argument is a Matrix, and
|
|
82
|
+
# ErrDimensionMismatch unless it has two columns.
|
|
83
|
+
#
|
|
84
|
+
# @param matrix [::Matrix] a matrix with two columns
|
|
85
|
+
# @return [self]
|
|
86
|
+
def transform(matrix)
|
|
87
|
+
raise TypeError, "#{matrix.class} is not a Matrix" unless MatrixInterop.matrix?(matrix)
|
|
88
|
+
|
|
89
|
+
result = matrix * to_vector
|
|
90
|
+
build(result[0], result[1])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# The permissive constructor. Everything Vector2d.parse accepts is
|
|
5
|
+
# recognized here, and turned into two coordinates the class builds a
|
|
6
|
+
# vector from. It is a module rather than a plain class method, so a
|
|
7
|
+
# subclass parses into its own kind.
|
|
8
|
+
module Parsing
|
|
9
|
+
# Matches a single coordinate in a string, with an optional sign and
|
|
10
|
+
# an optional fractional part.
|
|
11
|
+
COORDINATE_EXPRESSION = /[+-]?(?:\d+(?:\.\d+)?|\.\d+)/
|
|
12
|
+
|
|
13
|
+
# Matches the string form of a vector, "150x100" or "150,100".
|
|
14
|
+
# Whitespace is insignificant, the separator is case insensitive, and
|
|
15
|
+
# either coordinate can be left out to mean zero.
|
|
16
|
+
STRING_EXPRESSION = /
|
|
17
|
+
\A\s*(#{COORDINATE_EXPRESSION})?\s*[x,]\s*(#{COORDINATE_EXPRESSION})?\s*\z
|
|
18
|
+
/xi
|
|
19
|
+
|
|
20
|
+
# Stands in for an omitted second argument to .parse, so an explicit
|
|
21
|
+
# nil can be rejected as a coordinate.
|
|
22
|
+
NO_ARGUMENT = Object.new.freeze
|
|
23
|
+
|
|
24
|
+
private_constant :COORDINATE_EXPRESSION, :STRING_EXPRESSION, :NO_ARGUMENT
|
|
25
|
+
|
|
26
|
+
# Parses a vector out of any of the forms below, and is the
|
|
27
|
+
# permissive counterpart to .new, which takes exactly two
|
|
28
|
+
# coordinates. Vector2d() is shorthand for this method.
|
|
29
|
+
#
|
|
30
|
+
# Vector2d.parse(150, 100)
|
|
31
|
+
# Vector2d.parse(150.0, 100.0)
|
|
32
|
+
# Vector2d.parse("150x100")
|
|
33
|
+
# Vector2d.parse("150.0x100.0")
|
|
34
|
+
# Vector2d.parse([150,100])
|
|
35
|
+
# Vector2d.parse({x: 150, y: 100})
|
|
36
|
+
# Vector2d.parse({"x" => 150.0, "y" => 100.0})
|
|
37
|
+
# Vector2d.parse(Vector2d(150, 100))
|
|
38
|
+
# Vector2d.parse(Vector[150, 100])
|
|
39
|
+
# Vector2d.parse(Matrix[[150], [100]])
|
|
40
|
+
#
|
|
41
|
+
# Strings are either "150x100" or "150,100", optionally signed and
|
|
42
|
+
# case insensitive. Coordinates keep their type, so "150x100" gives
|
|
43
|
+
# integers and "150.0x100" gives a float and an integer. An omitted
|
|
44
|
+
# coordinate is zero, as in "x100".
|
|
45
|
+
#
|
|
46
|
+
# Vector2d.parse("-150X100") # => Vector2d(-150,100)
|
|
47
|
+
# Vector2d.parse("150, 100") # => Vector2d(150,100)
|
|
48
|
+
# Vector2d.parse("x100") # => Vector2d(0,100)
|
|
49
|
+
#
|
|
50
|
+
# A vector is returned as it is, as long as it is an instance of
|
|
51
|
+
# the class parsing it. A vector of any other class is rebuilt, so
|
|
52
|
+
# a subclass is always handed back its own kind.
|
|
53
|
+
#
|
|
54
|
+
# v = Vector2d(150, 100)
|
|
55
|
+
# Vector2d.parse(v).equal?(v) # => true
|
|
56
|
+
#
|
|
57
|
+
# Raises ArgumentError unless both coordinates resolve to real
|
|
58
|
+
# numbers. Complex numbers are not coordinates, and are rejected.
|
|
59
|
+
#
|
|
60
|
+
# Vector2d.parse(150, nil) # => ArgumentError
|
|
61
|
+
# Vector2d.parse(Complex(1, 2), 3) # => ArgumentError
|
|
62
|
+
#
|
|
63
|
+
# @param arg [Vector2d, Array, String, Hash, Integer, Float,
|
|
64
|
+
# Rational, BigDecimal, ::Vector, ::Matrix] the vector, in any of
|
|
65
|
+
# the forms above, or its x coordinate
|
|
66
|
+
# @param second_arg [Integer, Float, Rational, BigDecimal]
|
|
67
|
+
# the y coordinate, when the first argument is the x coordinate
|
|
68
|
+
# @return [Vector2d] a vector of this class, or the argument itself
|
|
69
|
+
# when it already is one
|
|
70
|
+
def parse(arg, second_arg = NO_ARGUMENT)
|
|
71
|
+
return parse_single_arg(arg) if NO_ARGUMENT.equal?(second_arg)
|
|
72
|
+
|
|
73
|
+
build(coordinate(arg), coordinate(second_arg))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def parse_single_arg(arg)
|
|
79
|
+
return parse_vector2d(arg) if arg.is_a?(Vector2d)
|
|
80
|
+
return parse_array(arg) if arg.is_a?(Array)
|
|
81
|
+
return parse_str(arg) if arg.is_a?(String)
|
|
82
|
+
return parse_hash(arg) if arg.is_a?(Hash)
|
|
83
|
+
return parse_vector(arg) if MatrixInterop.vector?(arg)
|
|
84
|
+
return parse_matrix(arg) if MatrixInterop.matrix?(arg)
|
|
85
|
+
|
|
86
|
+
value = coordinate(arg)
|
|
87
|
+
build(value, value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_array(array)
|
|
91
|
+
case array.length
|
|
92
|
+
when 1 then parse_single_arg(array.first)
|
|
93
|
+
when 2 then build(coordinate(array[0]), coordinate(array[1]))
|
|
94
|
+
else
|
|
95
|
+
raise ArgumentError, "expected 1 or 2 coordinates, got #{array.length}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_hash(hash)
|
|
100
|
+
build(coordinate(hash[:x] || hash["x"]),
|
|
101
|
+
coordinate(hash[:y] || hash["y"]))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parse_matrix(matrix)
|
|
105
|
+
shape = [matrix.row_count, matrix.column_count]
|
|
106
|
+
unless [[2, 1], [1, 2]].include?(shape)
|
|
107
|
+
raise ArgumentError,
|
|
108
|
+
"expected a 2x1 or 1x2 matrix, got #{shape.join('x')}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
values = matrix.to_a.flatten
|
|
112
|
+
build(coordinate(values[0]), coordinate(values[1]))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_vector(vector)
|
|
116
|
+
raise ArgumentError, "expected 2 coordinates, got #{vector.size}" unless vector.size == 2
|
|
117
|
+
|
|
118
|
+
build(coordinate(vector[0]), coordinate(vector[1]))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns a vector of this class as it is, and rebuilds one of any
|
|
122
|
+
# other class.
|
|
123
|
+
def parse_vector2d(vector)
|
|
124
|
+
return vector if vector.is_a?(self)
|
|
125
|
+
|
|
126
|
+
build(vector.x, vector.y)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_str(str)
|
|
130
|
+
match = STRING_EXPRESSION.match(str)
|
|
131
|
+
raise ArgumentError, "not a valid string input: #{str.inspect}" unless match
|
|
132
|
+
|
|
133
|
+
build(string_coordinate(match[1]), string_coordinate(match[2]))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def string_coordinate(value)
|
|
137
|
+
return 0 if value.nil?
|
|
138
|
+
|
|
139
|
+
value.include?(".") ? value.to_f : value.to_i
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|