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,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