vector2d 2.2.4 → 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/dependabot.yml +39 -0
- data/.github/workflows/build.yml +48 -10
- data/.gitignore +3 -1
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +5 -2
- data/.tool-versions +1 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +224 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +30 -0
- data/Gemfile +6 -2
- data/Gemfile.lock +85 -51
- data/README.md +330 -13
- data/Rakefile +21 -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 +174 -62
- data/release-please-config.json +10 -0
- 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 +9 -3
- metadata +55 -19
- 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 -85
- 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 -122
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
describe Vector2d::Comparison do
|
|
6
|
+
subject(:vector) { Vector2d.new(2, 3) }
|
|
7
|
+
|
|
8
|
+
describe "#approx_equal?" do
|
|
9
|
+
subject { vector.approx_equal?(other) }
|
|
10
|
+
|
|
11
|
+
context "when the vectors are equal" do
|
|
12
|
+
let(:other) { Vector2d.new(2, 3) }
|
|
13
|
+
|
|
14
|
+
it { is_expected.to be(true) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context "when the vectors differ by a rounding error" do
|
|
18
|
+
let(:vector) { Vector2d.new(0.1, 0.2) + Vector2d.new(0.2, 0.4) }
|
|
19
|
+
let(:other) { Vector2d.new(0.3, 0.6) }
|
|
20
|
+
|
|
21
|
+
it { is_expected.to be(true) }
|
|
22
|
+
|
|
23
|
+
it "is more than #== accepts" do
|
|
24
|
+
expect(vector).not_to eq(other)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context "when the vectors differ" do
|
|
29
|
+
let(:other) { Vector2d.new(2, 4) }
|
|
30
|
+
|
|
31
|
+
it { is_expected.to be(false) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context "when the other vector is coercible" do
|
|
35
|
+
let(:other) { [2, 3] }
|
|
36
|
+
|
|
37
|
+
it { is_expected.to be(true) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "is symmetric" do
|
|
41
|
+
other = Vector2d.new(0.1, 0.2) + Vector2d.new(0.2, 0.4)
|
|
42
|
+
|
|
43
|
+
expect(Vector2d.new(0.3, 0.6)).to be_approx_equal(other)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "scales the tolerance with the magnitudes" do
|
|
47
|
+
big = Vector2d.new(1e8, 2e8)
|
|
48
|
+
|
|
49
|
+
expect(big).to be_approx_equal(big.rotate(2 * Math::PI))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "doesn't accept the same drift at unit scale" do
|
|
53
|
+
expect(Vector2d.new(1, 2))
|
|
54
|
+
.not_to be_approx_equal(Vector2d.new(1, 2 + 5.5e-8))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context "with a tolerance" do
|
|
58
|
+
it "takes it as an absolute distance" do
|
|
59
|
+
expect(vector).to be_approx_equal(Vector2d.new(2, 4), 1.5)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "is false outside the tolerance" do
|
|
63
|
+
expect(vector).not_to be_approx_equal(Vector2d.new(2, 4), 0.5)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "includes the tolerance itself" do
|
|
67
|
+
expect(vector).to be_approx_equal(Vector2d.new(2, 4), 1.0)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "raises an error on a complex tolerance" do
|
|
71
|
+
expect { vector.approx_equal?(vector, Complex(1, 2)) }.to(
|
|
72
|
+
raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "#parallel?" do
|
|
79
|
+
subject { vector.parallel?(other) }
|
|
80
|
+
|
|
81
|
+
context "when the other vector points the same way" do
|
|
82
|
+
let(:other) { Vector2d.new(4, 6) }
|
|
83
|
+
|
|
84
|
+
it { is_expected.to be(true) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "when the other vector points the opposite way" do
|
|
88
|
+
let(:other) { Vector2d.new(-4, -6) }
|
|
89
|
+
|
|
90
|
+
it { is_expected.to be(true) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "when the vectors aren't parallel" do
|
|
94
|
+
let(:other) { Vector2d.new(3, 2) }
|
|
95
|
+
|
|
96
|
+
it { is_expected.to be(false) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context "with the zero vector" do
|
|
100
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
101
|
+
|
|
102
|
+
it { is_expected.to be(true) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context "when both vectors are zero" do
|
|
106
|
+
let(:vector) { Vector2d.new(0, 0) }
|
|
107
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
108
|
+
|
|
109
|
+
it { is_expected.to be(true) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "coerces the argument" do
|
|
113
|
+
expect(vector.parallel?("4x6")).to be(true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "is true for any vector and a multiple of it" do
|
|
117
|
+
vectors = (1..50).flat_map do |x|
|
|
118
|
+
(1..50).map { |y| Vector2d.new(x, y) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
expect(vectors).to all(satisfy { |v| v.parallel?(v * 3.7) })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context "with large vectors" do
|
|
125
|
+
let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
|
|
126
|
+
|
|
127
|
+
it "is true for a multiple of itself" do
|
|
128
|
+
expect(vector.parallel?(vector * Math::PI)).to be(true)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "is false for a vector that is barely off" do
|
|
132
|
+
expect(vector.parallel?(Vector2d.new(1.3e8, 7.7e8 + 1))).to be(false)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "#independent?" do
|
|
138
|
+
subject { vector.independent?(other) }
|
|
139
|
+
|
|
140
|
+
context "when the vectors aren't parallel" do
|
|
141
|
+
let(:other) { Vector2d.new(3, 2) }
|
|
142
|
+
|
|
143
|
+
it { is_expected.to be(true) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context "when the other vector points the same way" do
|
|
147
|
+
let(:other) { Vector2d.new(4, 6) }
|
|
148
|
+
|
|
149
|
+
it { is_expected.to be(false) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context "with the zero vector" do
|
|
153
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
154
|
+
|
|
155
|
+
it { is_expected.to be(false) }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe "#opposite?" do
|
|
160
|
+
subject { vector.opposite?(other) }
|
|
161
|
+
|
|
162
|
+
context "when the other vector points the opposite way" do
|
|
163
|
+
let(:other) { Vector2d.new(-2, -3) }
|
|
164
|
+
|
|
165
|
+
it { is_expected.to be(true) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
context "when the other vector is a negative multiple" do
|
|
169
|
+
let(:other) { Vector2d.new(-4, -6) }
|
|
170
|
+
|
|
171
|
+
it { is_expected.to be(true) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
context "when the other vector points the same way" do
|
|
175
|
+
let(:other) { Vector2d.new(4, 6) }
|
|
176
|
+
|
|
177
|
+
it { is_expected.to be(false) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
context "when the vectors aren't parallel" do
|
|
181
|
+
let(:other) { Vector2d.new(3, 2) }
|
|
182
|
+
|
|
183
|
+
it { is_expected.to be(false) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
context "when the other vector is perpendicular" do
|
|
187
|
+
let(:other) { Vector2d.new(-3, 2) }
|
|
188
|
+
|
|
189
|
+
it { is_expected.to be(false) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
context "with the zero vector" do
|
|
193
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
194
|
+
|
|
195
|
+
it { is_expected.to be(false) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
context "when the receiver is zero" do
|
|
199
|
+
let(:vector) { Vector2d.new(0, 0) }
|
|
200
|
+
let(:other) { Vector2d.new(2, 3) }
|
|
201
|
+
|
|
202
|
+
it { is_expected.to be(false) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context "when both vectors are zero" do
|
|
206
|
+
let(:vector) { Vector2d.new(0, 0) }
|
|
207
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
208
|
+
|
|
209
|
+
it { is_expected.to be(false) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "coerces the argument" do
|
|
213
|
+
expect(vector.opposite?("-2x-3")).to be(true)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "ignores the magnitude of the vectors" do
|
|
217
|
+
expect(vector.opposite?(vector * -1e9)).to be(true)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "is true for any vector and a negative multiple of it" do
|
|
221
|
+
vectors = (1..50).flat_map do |x|
|
|
222
|
+
(1..50).map { |y| Vector2d.new(x, y) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
expect(vectors).to all(satisfy { |v| v.opposite?(v * -3.7) })
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "is true for the reverse of the vector" do
|
|
229
|
+
expect(vector.opposite?(vector.reverse)).to be(true)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
context "with large vectors" do
|
|
233
|
+
let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
|
|
234
|
+
|
|
235
|
+
it "is true for a negative multiple of itself" do
|
|
236
|
+
expect(vector.opposite?(vector * -Math::PI)).to be(true)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "is false for a vector that is barely off" do
|
|
240
|
+
expect(vector.opposite?(Vector2d.new(-1.3e8, -7.7e8 - 1))).to be(false)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
describe "#perpendicular?" do
|
|
246
|
+
subject { vector.perpendicular?(other) }
|
|
247
|
+
|
|
248
|
+
context "when the other vector is perpendicular" do
|
|
249
|
+
let(:other) { Vector2d.new(-3, 2) }
|
|
250
|
+
|
|
251
|
+
it { is_expected.to be(true) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
context "when the other vector is perpendicular the other way" do
|
|
255
|
+
let(:other) { Vector2d.new(3, -2) }
|
|
256
|
+
|
|
257
|
+
it { is_expected.to be(true) }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
context "when the vectors aren't perpendicular" do
|
|
261
|
+
let(:other) { Vector2d.new(3, 2) }
|
|
262
|
+
|
|
263
|
+
it { is_expected.to be(false) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
context "with the zero vector" do
|
|
267
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
268
|
+
|
|
269
|
+
it { is_expected.to be(true) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
context "when both vectors are zero" do
|
|
273
|
+
let(:vector) { Vector2d.new(0, 0) }
|
|
274
|
+
let(:other) { Vector2d.new(0, 0) }
|
|
275
|
+
|
|
276
|
+
it { is_expected.to be(true) }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "coerces the argument" do
|
|
280
|
+
expect(vector.perpendicular?("-3x2")).to be(true)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it "is true for any vector rotated a quarter turn" do
|
|
284
|
+
vectors = (1..50).flat_map do |x|
|
|
285
|
+
(1..50).map { |y| Vector2d.new(x, y) }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
expect(vectors)
|
|
289
|
+
.to all(satisfy { |v| v.perpendicular?(v.rotate(Math::PI / 2)) })
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
context "with large vectors" do
|
|
293
|
+
let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
|
|
294
|
+
|
|
295
|
+
it "is true for a quarter turn" do
|
|
296
|
+
expect(vector.perpendicular?(vector.rotate(Math::PI / 2)))
|
|
297
|
+
.to be(true)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "is false for a vector that is barely off" do
|
|
301
|
+
expect(vector.perpendicular?(Vector2d.new(-7.7e8, 1.3e8 + 1)))
|
|
302
|
+
.to be(false)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
describe "#finite?" do
|
|
308
|
+
subject { vector.finite? }
|
|
309
|
+
|
|
310
|
+
context "when both coordinates are finite" do
|
|
311
|
+
it { is_expected.to be(true) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
context "when x is infinite" do
|
|
315
|
+
let(:vector) { Vector2d.new(Float::INFINITY, 3) }
|
|
316
|
+
|
|
317
|
+
it { is_expected.to be(false) }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context "when y is infinite" do
|
|
321
|
+
let(:vector) { Vector2d.new(2, -Float::INFINITY) }
|
|
322
|
+
|
|
323
|
+
it { is_expected.to be(false) }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
context "when a coordinate is NaN" do
|
|
327
|
+
let(:vector) { Vector2d.new(2, Float::NAN) }
|
|
328
|
+
|
|
329
|
+
it { is_expected.to be(false) }
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe "#nan?" do
|
|
334
|
+
subject { vector.nan? }
|
|
335
|
+
|
|
336
|
+
context "when neither coordinate is NaN" do
|
|
337
|
+
it { is_expected.to be(false) }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
context "when x is NaN" do
|
|
341
|
+
let(:vector) { Vector2d.new(Float::NAN, 3) }
|
|
342
|
+
|
|
343
|
+
it { is_expected.to be(true) }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
context "when y is NaN" do
|
|
347
|
+
let(:vector) { Vector2d.new(2, Float::NAN) }
|
|
348
|
+
|
|
349
|
+
it { is_expected.to be(true) }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
context "when a coordinate is infinite" do
|
|
353
|
+
let(:vector) { Vector2d.new(2, Float::INFINITY) }
|
|
354
|
+
|
|
355
|
+
it { is_expected.to be(false) }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
describe Vector2d::Componentwise do
|
|
6
|
+
subject(:vector) { Vector2d.new(2, 3) }
|
|
7
|
+
|
|
8
|
+
describe "#abs" do
|
|
9
|
+
it "returns the absolute value of each axis" do
|
|
10
|
+
expect(Vector2d.new(-2, 3).abs).to eq(Vector2d.new(2, 3))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "leaves a positive vector alone" do
|
|
14
|
+
expect(vector.abs).to eq(vector)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "is component-wise, not the length" do
|
|
18
|
+
expect(Vector2d.new(-2, -3).abs).to eq(Vector2d.new(2, 3))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#ceil" do
|
|
23
|
+
subject(:vector) { Vector2d.new(2.3, 3.3) }
|
|
24
|
+
|
|
25
|
+
it "rounds the vector up" do
|
|
26
|
+
expect(vector.ceil).to eq(Vector2d.new(3, 4))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "rounds up to the given precision" do
|
|
30
|
+
expect(Vector2d.new(2.441, 3.666).ceil(2))
|
|
31
|
+
.to eq(Vector2d.new(2.45, 3.67))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "rounds up to a negative precision" do
|
|
35
|
+
expect(Vector2d.new(24.4, 36.6).ceil(-1)).to eq(Vector2d.new(30, 40))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#floor" do
|
|
40
|
+
subject(:vector) { Vector2d.new(2.7, 3.6) }
|
|
41
|
+
|
|
42
|
+
it "rounds the vector down" do
|
|
43
|
+
expect(vector.floor).to eq(Vector2d.new(2, 3))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "rounds down to the given precision" do
|
|
47
|
+
expect(Vector2d.new(2.444, 3.669).floor(2))
|
|
48
|
+
.to eq(Vector2d.new(2.44, 3.66))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "rounds down to a negative precision" do
|
|
52
|
+
expect(Vector2d.new(24.4, 36.6).floor(-1)).to eq(Vector2d.new(20, 30))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "#round" do
|
|
57
|
+
let(:vector) { Vector2d.new(2.3333, 3.666) }
|
|
58
|
+
|
|
59
|
+
context "without argument" do
|
|
60
|
+
subject { vector.round }
|
|
61
|
+
|
|
62
|
+
it { is_expected.to eq(Vector2d.new(2, 4)) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "with precision" do
|
|
66
|
+
subject { vector.round(2) }
|
|
67
|
+
|
|
68
|
+
it { is_expected.to eq(Vector2d.new(2.33, 3.67)) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "#truncate" do
|
|
73
|
+
subject(:vector) { Vector2d.new(2, 3).truncate(2.5) }
|
|
74
|
+
|
|
75
|
+
it_behaves_like "a deprecated method", "truncate", "#limit_length"
|
|
76
|
+
|
|
77
|
+
it "limits the length, as #limit_length does" do
|
|
78
|
+
expect(vector.length).to be_within(0.0001).of(2.5)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "#sign" do
|
|
83
|
+
it "returns the sign of each axis" do
|
|
84
|
+
expect(Vector2d.new(-2, 3).sign).to eq(Vector2d.new(-1, 1))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "is zero for an axis that is zero" do
|
|
88
|
+
expect(Vector2d.new(0, -3).sign).to eq(Vector2d.new(0, -1))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "returns integers for a float vector" do
|
|
92
|
+
expect(Vector2d.new(-2.5, 0.0).sign.to_a).to eq([-1, 0])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "treats negative zero as zero" do
|
|
96
|
+
expect(Vector2d.new(-0.0, 1).sign).to eq(Vector2d.new(0, 1))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context "when a coordinate is NaN" do
|
|
100
|
+
it "raises an error" do
|
|
101
|
+
expect { Vector2d.new(Float::NAN, 3).sign }.to(
|
|
102
|
+
raise_error(ArgumentError, "NaN has no sign")
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "#snap" do
|
|
109
|
+
subject(:vector) { Vector2d.new(23, 47) }
|
|
110
|
+
|
|
111
|
+
it "snaps each axis to the nearest multiple" do
|
|
112
|
+
expect(vector.snap(10)).to eq(Vector2d.new(20, 50))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "takes a step per axis" do
|
|
116
|
+
expect(vector.snap(Vector2d.new(10, 5))).to eq(Vector2d.new(20, 45))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "coerces the step" do
|
|
120
|
+
expect(vector.snap("10x5")).to eq(Vector2d.new(20, 45))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "leaves a vector on the grid alone" do
|
|
124
|
+
expect(Vector2d.new(20, 50).snap(10)).to eq(Vector2d.new(20, 50))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "snaps negative coordinates away from zero at the halfway point" do
|
|
128
|
+
expect(Vector2d.new(-15, -25).snap(10)).to eq(Vector2d.new(-20, -30))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "handles a negative step" do
|
|
132
|
+
expect(vector.snap(-10)).to eq(Vector2d.new(20, 50))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
context "with a fractional step" do
|
|
136
|
+
subject(:vector) { Vector2d.new(2.3, 3.7) }
|
|
137
|
+
|
|
138
|
+
it "snaps to the fractional grid" do
|
|
139
|
+
expect(vector.snap(0.5)).to eq(Vector2d.new(2.5, 3.5))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "keeps the type of the step" do
|
|
143
|
+
expect(vector.snap(1)).to eql(Vector2d.new(2, 4))
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
context "when the step is zero" do
|
|
148
|
+
it "leaves both axes unchanged" do
|
|
149
|
+
expect(vector.snap(0)).to eq(vector)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "leaves only the unstepped axis unchanged" do
|
|
153
|
+
expect(vector.snap(Vector2d.new(10, 0))).to eq(Vector2d.new(20, 47))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "#max" do
|
|
159
|
+
subject(:vector) { Vector2d.new(2, 8) }
|
|
160
|
+
|
|
161
|
+
it "returns the larger value of each axis" do
|
|
162
|
+
expect(vector.max(Vector2d.new(5, 5))).to eq(Vector2d.new(5, 8))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "coerces the argument" do
|
|
166
|
+
expect(vector.max(5)).to eq(Vector2d.new(5, 8))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "is symmetric" do
|
|
170
|
+
other = Vector2d.new(5, 5)
|
|
171
|
+
expect(other.max(vector)).to eq(vector.max(other))
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe "#min" do
|
|
176
|
+
subject(:vector) { Vector2d.new(2, 8) }
|
|
177
|
+
|
|
178
|
+
it "returns the smaller value of each axis" do
|
|
179
|
+
expect(vector.min(Vector2d.new(5, 5))).to eq(Vector2d.new(2, 5))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "coerces the argument" do
|
|
183
|
+
expect(vector.min(5)).to eq(Vector2d.new(2, 5))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "is symmetric" do
|
|
187
|
+
other = Vector2d.new(5, 5)
|
|
188
|
+
expect(other.min(vector)).to eq(vector.min(other))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
describe "#clamp" do
|
|
193
|
+
subject(:vector) { Vector2d.new(2, 8) }
|
|
194
|
+
|
|
195
|
+
it "clamps each axis" do
|
|
196
|
+
expect(vector.clamp(Vector2d.new(3, 3), Vector2d.new(6, 6)))
|
|
197
|
+
.to eq(Vector2d.new(3, 6))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "coerces the bounds" do
|
|
201
|
+
expect(vector.clamp(3, 6)).to eq(Vector2d.new(3, 6))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "leaves a vector within the bounds alone" do
|
|
205
|
+
expect(vector.clamp(0, 10)).to eq(vector)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "raises ArgumentError without an upper bound" do
|
|
209
|
+
expect { vector.clamp(3) }.to raise_error(ArgumentError)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context "with a range" do
|
|
213
|
+
it "clamps each axis" do
|
|
214
|
+
expect(vector.clamp(3..6)).to eq(Vector2d.new(3, 6))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "coerces the bounds" do
|
|
218
|
+
expect(vector.clamp("3x3".."6x6")).to eq(Vector2d.new(3, 6))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "leaves a vector within the bounds alone" do
|
|
222
|
+
expect(vector.clamp(0..10)).to eq(vector)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "matches the two argument form" do
|
|
226
|
+
expect(vector.clamp(3..6)).to eq(vector.clamp(3, 6))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "clamps only the upper bound of a beginless range" do
|
|
230
|
+
expect(vector.clamp(..6)).to eq(Vector2d.new(2, 6))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "clamps only the lower bound of an endless range" do
|
|
234
|
+
expect(vector.clamp(3..)).to eq(Vector2d.new(3, 8))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "leaves a vector bounded at neither end alone" do
|
|
238
|
+
expect(vector.clamp(nil..nil)).to eq(vector)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "matches #min for a beginless range" do
|
|
242
|
+
expect(vector.clamp(..6)).to eq(vector.min(6))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "matches #max for an endless range" do
|
|
246
|
+
expect(vector.clamp(3..)).to eq(vector.max(3))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "raises ArgumentError for an exclusive range" do
|
|
250
|
+
expect { vector.clamp(3...6) }.to raise_error(ArgumentError)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "raises ArgumentError when given a second argument" do
|
|
254
|
+
expect { vector.clamp(3..6, 6) }.to raise_error(ArgumentError)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe "#with_x" do
|
|
260
|
+
it "replaces x" do
|
|
261
|
+
expect(vector.with_x(5)).to eq(Vector2d.new(5, 3))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "keeps the type of the new coordinate" do
|
|
265
|
+
expect(vector.with_x(5.0).x).to be_a(Float)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "leaves the receiver alone" do
|
|
269
|
+
expect { vector.with_x(5) }.not_to change(vector, :to_a)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "raises an error on a string" do
|
|
273
|
+
expect { vector.with_x("5") }.to(
|
|
274
|
+
raise_error(ArgumentError, 'not a valid coordinate: "5"')
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "raises an error on nil" do
|
|
279
|
+
expect { vector.with_x(nil) }.to(
|
|
280
|
+
raise_error(ArgumentError, "not a valid coordinate: nil")
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
describe "#with_y" do
|
|
286
|
+
it "replaces y" do
|
|
287
|
+
expect(vector.with_y(5)).to eq(Vector2d.new(2, 5))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "keeps the type of the new coordinate" do
|
|
291
|
+
expect(vector.with_y(5.0).y).to be_a(Float)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it "raises an error on a vector" do
|
|
295
|
+
expect { vector.with_y(Vector2d.new(2, 3)) }.to(
|
|
296
|
+
raise_error(ArgumentError, "not a valid coordinate: Vector2d(2,3)")
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|