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,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# Operations applied to each coordinate on its own, the way Numeric
|
|
5
|
+
# applies them to a number.
|
|
6
|
+
module Componentwise
|
|
7
|
+
# Returns the absolute value of each axis. This is component-wise,
|
|
8
|
+
# not the magnitude of the vector, which is #length.
|
|
9
|
+
#
|
|
10
|
+
# Vector2d(-2, 3).abs # => Vector2d(2,3)
|
|
11
|
+
# Vector2d(-2, -3).abs # => Vector2d(2,3)
|
|
12
|
+
#
|
|
13
|
+
# @return [self]
|
|
14
|
+
def abs
|
|
15
|
+
build(x.abs, y.abs)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Rounds vector up to nearest integer.
|
|
19
|
+
#
|
|
20
|
+
# Vector2d(2.4, 3.6).ceil # => Vector2d(3,4)
|
|
21
|
+
# Vector2d(2.441, 3.666).ceil(2) # => Vector2d(2.45,3.67)
|
|
22
|
+
#
|
|
23
|
+
# @param digits [Integer] the number of decimal places to keep
|
|
24
|
+
# @return [self]
|
|
25
|
+
def ceil(digits = 0)
|
|
26
|
+
build(x.ceil(digits), y.ceil(digits))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Rounds vector down to nearest integer.
|
|
30
|
+
#
|
|
31
|
+
# Vector2d(2.4, 3.6).floor # => Vector2d(2,3)
|
|
32
|
+
# Vector2d(2.444, 3.669).floor(2) # => Vector2d(2.44,3.66)
|
|
33
|
+
#
|
|
34
|
+
# @param digits [Integer] the number of decimal places to keep
|
|
35
|
+
# @return [self]
|
|
36
|
+
def floor(digits = 0)
|
|
37
|
+
build(x.floor(digits), y.floor(digits))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Rounds vector to nearest integer.
|
|
41
|
+
#
|
|
42
|
+
# Vector2d(2.4, 3.6).round # => Vector2d(2,4)
|
|
43
|
+
# Vector2d(2.4444, 3.666).round(2) # => Vector2d(2.44,3.67)
|
|
44
|
+
#
|
|
45
|
+
# @param digits [Integer] the number of decimal places to keep
|
|
46
|
+
# @return [self]
|
|
47
|
+
def round(digits = 0)
|
|
48
|
+
build(x.round(digits), y.round(digits))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @deprecated Use #limit_length instead. The name belongs to the
|
|
52
|
+
# #ceil/#floor/#round family, which maps Numeric over both
|
|
53
|
+
# coordinates.
|
|
54
|
+
#
|
|
55
|
+
# @param max [Integer, Float, Rational, BigDecimal] the maximum length
|
|
56
|
+
# @return [self]
|
|
57
|
+
def truncate(max)
|
|
58
|
+
warn_deprecated("Vector2d#truncate is deprecated. Use #limit_length instead.")
|
|
59
|
+
limit_length(max)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the sign of each axis, -1, 0 or 1.
|
|
63
|
+
#
|
|
64
|
+
# Vector2d(-2, 3).sign # => Vector2d(-1,1)
|
|
65
|
+
# Vector2d(0, -3).sign # => Vector2d(0,-1)
|
|
66
|
+
#
|
|
67
|
+
# The signs are integers, whatever the coordinates were. There are
|
|
68
|
+
# only three of them, and they are exact.
|
|
69
|
+
#
|
|
70
|
+
# Vector2d(-2.5, 0.0).sign # => Vector2d(-1,0)
|
|
71
|
+
#
|
|
72
|
+
# NaN has no sign, so ArgumentError is raised.
|
|
73
|
+
#
|
|
74
|
+
# Vector2d(Float::NAN, 3).sign # => ArgumentError
|
|
75
|
+
#
|
|
76
|
+
# @return [self] a vector of -1, 0 and 1
|
|
77
|
+
def sign
|
|
78
|
+
build(coordinate_sign(x), coordinate_sign(y))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Snaps each axis to the nearest multiple of a step. The step is
|
|
82
|
+
# coerced, so scalars work too, and a vector gives each axis its
|
|
83
|
+
# own step.
|
|
84
|
+
#
|
|
85
|
+
# vector = Vector2d(23, 47)
|
|
86
|
+
# vector.snap(10) # => Vector2d(20,50)
|
|
87
|
+
# vector.snap(Vector2d(10, 5)) # => Vector2d(20,45)
|
|
88
|
+
#
|
|
89
|
+
# Coordinates take the type of the step, so an integer step snaps
|
|
90
|
+
# to integers.
|
|
91
|
+
#
|
|
92
|
+
# Vector2d(2.3, 3.7).snap(1) # => Vector2d(2,4)
|
|
93
|
+
# Vector2d(2.3, 3.7).snap(0.5) # => Vector2d(2.5,3.5)
|
|
94
|
+
#
|
|
95
|
+
# A step of zero has no multiples to snap to, and leaves the axis
|
|
96
|
+
# unchanged.
|
|
97
|
+
#
|
|
98
|
+
# vector.snap(0) # => Vector2d(23,47)
|
|
99
|
+
# vector.snap(Vector2d(10, 0)) # => Vector2d(20,47)
|
|
100
|
+
#
|
|
101
|
+
# @!macro coercible
|
|
102
|
+
# @return [self]
|
|
103
|
+
def snap(step)
|
|
104
|
+
v = coerce_vector(step)
|
|
105
|
+
build(snap_coordinate(x, v.x), snap_coordinate(y, v.y))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the larger value of each axis. The other vector is
|
|
109
|
+
# coerced, so scalars work too.
|
|
110
|
+
#
|
|
111
|
+
# vector = Vector2d(2, 8)
|
|
112
|
+
# vector.max(Vector2d(5, 5)) # => Vector2d(5,8)
|
|
113
|
+
# vector.max(5) # => Vector2d(5,8)
|
|
114
|
+
#
|
|
115
|
+
# @!macro coercible
|
|
116
|
+
# @return [self]
|
|
117
|
+
def max(other)
|
|
118
|
+
v = coerce_vector(other)
|
|
119
|
+
build([x, v.x].max, [y, v.y].max)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the smaller value of each axis. The other vector is
|
|
123
|
+
# coerced, so scalars work too.
|
|
124
|
+
#
|
|
125
|
+
# vector = Vector2d(2, 8)
|
|
126
|
+
# vector.min(Vector2d(5, 5)) # => Vector2d(2,5)
|
|
127
|
+
# vector.min(5) # => Vector2d(2,5)
|
|
128
|
+
#
|
|
129
|
+
# @!macro coercible
|
|
130
|
+
# @return [self]
|
|
131
|
+
def min(other)
|
|
132
|
+
v = coerce_vector(other)
|
|
133
|
+
build([x, v.x].min, [y, v.y].min)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Clamps the vector between two others, one axis at a time. The
|
|
137
|
+
# bounds are coerced, so scalars work too.
|
|
138
|
+
#
|
|
139
|
+
# vector = Vector2d(2, 8)
|
|
140
|
+
# vector.clamp(Vector2d(3, 3), Vector2d(6, 6)) # => Vector2d(3,6)
|
|
141
|
+
# vector.clamp(3, 6) # => Vector2d(3,6)
|
|
142
|
+
#
|
|
143
|
+
# The bounds can also be given as a single range, which may be
|
|
144
|
+
# beginless or endless to clamp only one side.
|
|
145
|
+
#
|
|
146
|
+
# vector.clamp(3..6) # => Vector2d(3,6)
|
|
147
|
+
# vector.clamp(..6) # => Vector2d(2,6)
|
|
148
|
+
# vector.clamp(3..) # => Vector2d(3,8)
|
|
149
|
+
#
|
|
150
|
+
# The range must not exclude its end, as with Comparable#clamp.
|
|
151
|
+
#
|
|
152
|
+
# vector.clamp(3...6) # => ArgumentError
|
|
153
|
+
#
|
|
154
|
+
# @overload clamp(min, max)
|
|
155
|
+
# @param min [Vector2d, Array, String, Hash, Integer, Float, Rational,
|
|
156
|
+
# BigDecimal, ::Vector, ::Matrix] the lower bound, anything
|
|
157
|
+
# Vector2d.parse accepts
|
|
158
|
+
# @param max [Vector2d, Array, String, Hash, Integer, Float, Rational,
|
|
159
|
+
# BigDecimal, ::Vector, ::Matrix] the upper bound
|
|
160
|
+
# @overload clamp(range)
|
|
161
|
+
# @param range [Range] both bounds, and may be beginless or endless
|
|
162
|
+
# @return [self]
|
|
163
|
+
def clamp(min, max = nil)
|
|
164
|
+
min_v, max_v = clamp_bounds(min, max)
|
|
165
|
+
build(
|
|
166
|
+
x.clamp(Range.new(min_v&.x, max_v&.x)),
|
|
167
|
+
y.clamp(Range.new(min_v&.y, max_v&.y))
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the vector with x replaced.
|
|
172
|
+
#
|
|
173
|
+
# Vector2d(2, 3).with_x(5) # => Vector2d(5,3)
|
|
174
|
+
#
|
|
175
|
+
# Vectors are immutable, so this is how a single axis is changed.
|
|
176
|
+
# The value is a coordinate, not a vector, and is not coerced.
|
|
177
|
+
#
|
|
178
|
+
# Vector2d(2, 3).with_x("5") # => ArgumentError
|
|
179
|
+
#
|
|
180
|
+
# @param value [Integer, Float, Rational, BigDecimal] the new x coordinate
|
|
181
|
+
# @return [self]
|
|
182
|
+
def with_x(value)
|
|
183
|
+
build(value, y)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the vector with y replaced.
|
|
187
|
+
#
|
|
188
|
+
# Vector2d(2, 3).with_y(5) # => Vector2d(2,5)
|
|
189
|
+
#
|
|
190
|
+
# The value is a coordinate, not a vector, and is not coerced.
|
|
191
|
+
#
|
|
192
|
+
# Vector2d(2, 3).with_y(nil) # => ArgumentError
|
|
193
|
+
#
|
|
194
|
+
# @param value [Integer, Float, Rational, BigDecimal] the new y coordinate
|
|
195
|
+
# @return [self]
|
|
196
|
+
def with_y(value)
|
|
197
|
+
build(x, value)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def clamp_bounds(min, max)
|
|
203
|
+
if min.is_a?(Range)
|
|
204
|
+
return range_bounds(min, max)
|
|
205
|
+
.map { |bound| bound && coerce_vector(bound) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if max.nil?
|
|
209
|
+
|
|
210
|
+
[coerce_vector(min), coerce_vector(max)]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Splits the range form of #clamp and #clamp_length into its two
|
|
214
|
+
# ends, either of which can be nil.
|
|
215
|
+
def range_bounds(range, max)
|
|
216
|
+
raise ArgumentError, "wrong number of arguments (given 2, expected 1)" unless max.nil?
|
|
217
|
+
raise ArgumentError, "cannot clamp with an exclusive range" if range.exclude_end?
|
|
218
|
+
|
|
219
|
+
[range.begin, range.end]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The sign of a coordinate. Comparing to zero gives one of -1, 0
|
|
223
|
+
# and 1, or nothing at all for NaN.
|
|
224
|
+
def coordinate_sign(value)
|
|
225
|
+
sign = value <=> 0
|
|
226
|
+
raise ArgumentError, "NaN has no sign" if sign.nil?
|
|
227
|
+
|
|
228
|
+
sign
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Rounds a coordinate to the nearest multiple of a step. A step of
|
|
232
|
+
# zero has no multiples, and the coordinate is left alone.
|
|
233
|
+
def snap_coordinate(value, step)
|
|
234
|
+
return value if step.zero?
|
|
235
|
+
|
|
236
|
+
(value / step.to_f).round * step
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# The constructors that build a vector from something other than
|
|
5
|
+
# coordinates: an angle, a random draw, or a direction. They are
|
|
6
|
+
# class methods rather than constants, so a subclass gets its own.
|
|
7
|
+
module Constructors
|
|
8
|
+
# Creates a vector from an angle in radians, with an optional
|
|
9
|
+
# length. Angles are measured counterclockwise from the positive x
|
|
10
|
+
# axis, the same convention #angle follows.
|
|
11
|
+
#
|
|
12
|
+
# Vector2d.from_angle(0) # => Vector2d(1.0,0.0)
|
|
13
|
+
# Vector2d.from_angle(Math::PI / 4) # => Vector2d(0.7071..,0.7071..)
|
|
14
|
+
# Vector2d.from_angle(Math::PI / 4, 2.0) # => Vector2d(1.4142..,1.4142..)
|
|
15
|
+
#
|
|
16
|
+
# Coordinates are always floats. This is the inverse of #to_polar.
|
|
17
|
+
#
|
|
18
|
+
# length, angle = Vector2d(2, 3).to_polar
|
|
19
|
+
# Vector2d.from_angle(angle, length) # => Vector2d(2.0,3.0)
|
|
20
|
+
#
|
|
21
|
+
# Raises ArgumentError unless both arguments are real numbers.
|
|
22
|
+
# Complex numbers are not coordinates, and are rejected.
|
|
23
|
+
#
|
|
24
|
+
# Vector2d.from_angle(Complex(1, 2)) # => ArgumentError
|
|
25
|
+
#
|
|
26
|
+
# @param angle [Integer, Float, Rational, BigDecimal] the angle in radians
|
|
27
|
+
# @param length [Integer, Float, Rational, BigDecimal]
|
|
28
|
+
# the length of the vector
|
|
29
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
30
|
+
def from_angle(angle, length = 1.0)
|
|
31
|
+
angle = coordinate(angle).to_f
|
|
32
|
+
length = coordinate(length).to_f
|
|
33
|
+
build(Math.cos(angle) * length, Math.sin(angle) * length)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Creates a random vector, uniformly distributed by angle, with an
|
|
37
|
+
# optional length. Coordinates are always floats, as with
|
|
38
|
+
# .from_angle.
|
|
39
|
+
#
|
|
40
|
+
# Vector2d.random.length.round(6) # => 1.0
|
|
41
|
+
# Vector2d.random(2.0).length.round(6) # => 2.0
|
|
42
|
+
#
|
|
43
|
+
# Pass a Random to draw from a seeded sequence.
|
|
44
|
+
#
|
|
45
|
+
# a = Vector2d.random(random: Random.new(42))
|
|
46
|
+
# b = Vector2d.random(random: Random.new(42))
|
|
47
|
+
# a == b # => true
|
|
48
|
+
#
|
|
49
|
+
# Raises ArgumentError unless the length is a real number.
|
|
50
|
+
#
|
|
51
|
+
# Vector2d.random(Complex(1, 2)) # => ArgumentError
|
|
52
|
+
#
|
|
53
|
+
# @param length [Integer, Float, Rational, BigDecimal]
|
|
54
|
+
# the length of the vector
|
|
55
|
+
# @param random [#rand] the source of randomness
|
|
56
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
57
|
+
def random(length = 1.0, random: Random)
|
|
58
|
+
from_angle(random.rand * 2 * Math::PI, length)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The zero vector.
|
|
62
|
+
#
|
|
63
|
+
# Vector2d.zero # => Vector2d(0,0)
|
|
64
|
+
#
|
|
65
|
+
# This and the five constants below have integer coordinates. They
|
|
66
|
+
# are exact, and integers keep them exact through arithmetic with
|
|
67
|
+
# integer vectors, widening to floats only when a float is
|
|
68
|
+
# involved. .from_angle returns floats instead, because a general
|
|
69
|
+
# angle has no exact coordinates.
|
|
70
|
+
#
|
|
71
|
+
# Vector2d.zero.x # => 0
|
|
72
|
+
# (Vector2d.up * 2).y # => 2
|
|
73
|
+
# (Vector2d.up * 0.5).y # => 0.5
|
|
74
|
+
#
|
|
75
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
76
|
+
def zero
|
|
77
|
+
build(0, 0)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The vector with both coordinates set to one.
|
|
81
|
+
#
|
|
82
|
+
# Vector2d.one # => Vector2d(1,1)
|
|
83
|
+
#
|
|
84
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
85
|
+
def one
|
|
86
|
+
build(1, 1)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The unit vector pointing up.
|
|
90
|
+
#
|
|
91
|
+
# Vector2d.up # => Vector2d(0,1)
|
|
92
|
+
# Vector2d.up.angle # => 1.5707..
|
|
93
|
+
#
|
|
94
|
+
# The y axis grows upwards here, and angles turn counterclockwise
|
|
95
|
+
# from the positive x axis. That is the convention .from_angle,
|
|
96
|
+
# #angle, #rotate and #perpendicular all follow. Libraries drawing
|
|
97
|
+
# in screen coordinates grow the y axis downwards and call (0, -1)
|
|
98
|
+
# up, so flip the y axis at that boundary.
|
|
99
|
+
#
|
|
100
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
101
|
+
def up
|
|
102
|
+
build(0, 1)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The unit vector pointing down. See .up for the direction the y
|
|
106
|
+
# axis grows in.
|
|
107
|
+
#
|
|
108
|
+
# Vector2d.down # => Vector2d(0,-1)
|
|
109
|
+
# Vector2d.down.angle # => -1.5707..
|
|
110
|
+
#
|
|
111
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
112
|
+
def down
|
|
113
|
+
build(0, -1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The unit vector pointing left.
|
|
117
|
+
#
|
|
118
|
+
# Vector2d.left # => Vector2d(-1,0)
|
|
119
|
+
# Vector2d.left.angle # => 3.1415..
|
|
120
|
+
#
|
|
121
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
122
|
+
def left
|
|
123
|
+
build(-1, 0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The unit vector pointing right, along the positive x axis. This
|
|
127
|
+
# is the direction angles are measured from.
|
|
128
|
+
#
|
|
129
|
+
# Vector2d.right # => Vector2d(1,0)
|
|
130
|
+
# Vector2d.right.angle # => 0.0
|
|
131
|
+
#
|
|
132
|
+
# @return [Vector2d] a vector of the receiver's class
|
|
133
|
+
def right
|
|
134
|
+
build(1, 0)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# Conversions to and from other Ruby objects, including the coercion
|
|
5
|
+
# protocol and pattern matching. Conversions to the standard library
|
|
6
|
+
# Matrix and Vector are in Vector2d::MatrixInterop.
|
|
7
|
+
module Conversions
|
|
8
|
+
# Implements Ruby's coercion protocol, so a vector can be the right
|
|
9
|
+
# hand operand of a scalar.
|
|
10
|
+
#
|
|
11
|
+
# 2 * Vector2d(3, 4) # => Vector2d(6,8)
|
|
12
|
+
#
|
|
13
|
+
# The operand is built through #build, so a subclass is the result
|
|
14
|
+
# on either side of the operator.
|
|
15
|
+
#
|
|
16
|
+
# Matrices are coerced the other way around, see
|
|
17
|
+
# Vector2d::MatrixInterop#coerce.
|
|
18
|
+
#
|
|
19
|
+
# @!macro coercible
|
|
20
|
+
# @return [Array(self, self)] the coerced operand and this vector
|
|
21
|
+
def coerce(other)
|
|
22
|
+
v = coerce_vector(other)
|
|
23
|
+
[build(v.x, v.y), self]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the components as an array, so a vector can be matched
|
|
27
|
+
# against an array pattern.
|
|
28
|
+
#
|
|
29
|
+
# Vector2d(3, 4).deconstruct # => [3,4]
|
|
30
|
+
#
|
|
31
|
+
# case Vector2d(3, 4)
|
|
32
|
+
# in [0, 0] then :origin
|
|
33
|
+
# in [Integer => a, Integer => b] then a + b
|
|
34
|
+
# end # => 7
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<Integer, Float, Rational, BigDecimal>] the [x, y] pair
|
|
37
|
+
def deconstruct
|
|
38
|
+
to_a
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the components as a hash, so a vector can be matched
|
|
42
|
+
# against a hash pattern. Both components are always returned,
|
|
43
|
+
# whichever keys the pattern asks for.
|
|
44
|
+
#
|
|
45
|
+
# Vector2d(3, 4).deconstruct_keys([:x]) # => {x: 3, y: 4}
|
|
46
|
+
#
|
|
47
|
+
# case Vector2d(0, 4)
|
|
48
|
+
# in {x: 0} then :on_y_axis
|
|
49
|
+
# in {y: 0} then :on_x_axis
|
|
50
|
+
# end # => :on_y_axis
|
|
51
|
+
#
|
|
52
|
+
# @param _keys [Array<Symbol>, nil] ignored, both components are
|
|
53
|
+
# always returned
|
|
54
|
+
# @return [Hash{Symbol => Integer, Float, Rational, BigDecimal}]
|
|
55
|
+
# the x and y coordinates
|
|
56
|
+
def deconstruct_keys(_keys)
|
|
57
|
+
to_hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Renders vector as a pretty string.
|
|
61
|
+
#
|
|
62
|
+
# Vector2d(2, 3).inspect # => "Vector2d(2,3)"
|
|
63
|
+
#
|
|
64
|
+
# @return [String]
|
|
65
|
+
def inspect
|
|
66
|
+
"#{self.class}(#{x},#{y})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Converts vector to array.
|
|
70
|
+
#
|
|
71
|
+
# Vector2d(2, 3).to_a # => [2,3]
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Integer, Float, Rational, BigDecimal>] the [x, y] pair
|
|
74
|
+
def to_a
|
|
75
|
+
[x, y]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Converts vector to hash.
|
|
79
|
+
#
|
|
80
|
+
# Vector2d(2, 3).to_hash # => {x: 2, y: 3}
|
|
81
|
+
#
|
|
82
|
+
# @return [Hash{Symbol => Integer, Float, Rational, BigDecimal}]
|
|
83
|
+
# the x and y coordinates
|
|
84
|
+
def to_hash
|
|
85
|
+
{ x: x, y: y }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Converts the coordinates to integers. The result is still a
|
|
89
|
+
# vector of this class, unlike #to_vector, which converts to the
|
|
90
|
+
# standard library Vector.
|
|
91
|
+
#
|
|
92
|
+
# Vector2d(2.0, 3.0).to_i_vector # => Vector2d(2,3)
|
|
93
|
+
#
|
|
94
|
+
# @return [self]
|
|
95
|
+
def to_i_vector
|
|
96
|
+
build(x.to_i, y.to_i)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Converts the coordinates to floats. As with #to_i_vector, the
|
|
100
|
+
# result is a vector of this class.
|
|
101
|
+
#
|
|
102
|
+
# Vector2d(2, 3).to_f_vector # => Vector2d(2.0,3.0)
|
|
103
|
+
#
|
|
104
|
+
# @return [self]
|
|
105
|
+
def to_f_vector
|
|
106
|
+
build(x.to_f, y.to_f)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Converts vector to string.
|
|
110
|
+
#
|
|
111
|
+
# Vector2d.new(150, 100).to_s # => "150x100"
|
|
112
|
+
#
|
|
113
|
+
# @return [String]
|
|
114
|
+
def to_s
|
|
115
|
+
"#{x}x#{y}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Parses anything Vector2d.parse accepts into a vector. A vector is
|
|
121
|
+
# returned as it is.
|
|
122
|
+
def coerce_vector(other)
|
|
123
|
+
return other if other.is_a?(Vector2d)
|
|
124
|
+
return Vector2d.parse(other) if parseable?(other)
|
|
125
|
+
|
|
126
|
+
raise TypeError, "#{other.class} can't be coerced into #{self.class}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Can the object be parsed into a vector? Complex numbers are
|
|
130
|
+
# Numeric, but they are not coordinates.
|
|
131
|
+
def parseable?(other)
|
|
132
|
+
case other
|
|
133
|
+
when Vector2d, Array, String, Hash then true
|
|
134
|
+
when Numeric then other.real?
|
|
135
|
+
else MatrixInterop.vector?(other) || MatrixInterop.matrix?(other)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# Coordinate validation, shared by the class methods that parse input
|
|
5
|
+
# and by the constructor.
|
|
6
|
+
module Coordinates
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Returns the value if it is a valid coordinate, raises
|
|
10
|
+
# ArgumentError otherwise. Coordinates are real numbers, so Complex
|
|
11
|
+
# is rejected along with everything outside Numeric.
|
|
12
|
+
#
|
|
13
|
+
# @param value [Object] the value to validate
|
|
14
|
+
# @return [Integer, Float, Rational, BigDecimal] the value
|
|
15
|
+
def coordinate(value)
|
|
16
|
+
return value if value.is_a?(Float) || value.is_a?(Integer)
|
|
17
|
+
raise ArgumentError, "not a valid coordinate: #{value.inspect}" unless value.is_a?(Numeric) && value.real?
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# The warning issued by the methods deprecated for 3.0. Each of them
|
|
5
|
+
# names its replacement, and forwards to it.
|
|
6
|
+
module Deprecation
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Warns that a method is deprecated. Silent unless deprecation
|
|
10
|
+
# warnings are enabled, either with <tt>Warning[:deprecated] = true</tt>
|
|
11
|
+
# or by running Ruby with <tt>-w</tt>.
|
|
12
|
+
def warn_deprecated(message)
|
|
13
|
+
warn(message, uplevel: 2, category: :deprecated)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|