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,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Vector2d::Lengths do
6
+ subject(:vector) { Vector2d.new(2, 3) }
7
+
8
+ describe "#length" do
9
+ it "calculates the length" do
10
+ expect(vector.length).to be_within(0.0001).of(3.6055)
11
+ end
12
+ end
13
+
14
+ describe "#magnitude" do
15
+ it "is an alias of #length" do
16
+ expect(vector.magnitude).to be_within(0.0001).of(3.6055)
17
+ end
18
+ end
19
+
20
+ describe "#norm" do
21
+ it "is an alias of #length" do
22
+ expect(vector.norm).to be_within(0.0001).of(3.6055)
23
+ end
24
+ end
25
+
26
+ describe "#length_squared" do
27
+ it "calculates the squared length" do
28
+ expect(vector.length_squared).to eq(13)
29
+ end
30
+ end
31
+
32
+ describe "#squared_length" do
33
+ subject(:vector) { Vector2d.new(2, 3).squared_length }
34
+
35
+ it_behaves_like "a deprecated method", "squared_length", "#length_squared"
36
+
37
+ it "returns the squared length, as #length_squared does" do
38
+ expect(vector).to eq(13)
39
+ end
40
+ end
41
+
42
+ describe "#zero?" do
43
+ subject { vector.zero? }
44
+
45
+ context "when vector is the zero vector" do
46
+ let(:vector) { Vector2d.new(0, 0) }
47
+
48
+ it { is_expected.to be(true) }
49
+ end
50
+
51
+ context "when vector isn't the zero vector" do
52
+ let(:vector) { Vector2d.new(2, 3) }
53
+
54
+ it { is_expected.to be(false) }
55
+ end
56
+ end
57
+
58
+ describe "#approx_zero?" do
59
+ subject { vector.approx_zero? }
60
+
61
+ context "when vector is the zero vector" do
62
+ let(:vector) { Vector2d.new(0, 0) }
63
+
64
+ it { is_expected.to be(true) }
65
+ end
66
+
67
+ context "when vector is a rounding error away from zero" do
68
+ let(:vector) { Vector2d.new(1e-17, 0) }
69
+
70
+ it { is_expected.to be(true) }
71
+ end
72
+
73
+ context "when vector is small but not that small" do
74
+ let(:vector) { Vector2d.new(1e-15, 0) }
75
+
76
+ it { is_expected.to be(false) }
77
+ end
78
+
79
+ context "when vector isn't the zero vector" do
80
+ it { is_expected.to be(false) }
81
+ end
82
+
83
+ context "with a tolerance" do
84
+ it "takes it as an absolute length" do
85
+ expect(Vector2d.new(0.2, 0)).to be_approx_zero(0.5)
86
+ end
87
+
88
+ it "is false outside the tolerance" do
89
+ expect(Vector2d.new(0.6, 0)).not_to be_approx_zero(0.5)
90
+ end
91
+
92
+ it "raises an error on a complex tolerance" do
93
+ expect { vector.approx_zero?(Complex(1, 2)) }.to(
94
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
95
+ )
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "#normalized?" do
101
+ subject { vector.normalized? }
102
+
103
+ context "when vector is normalized" do
104
+ let(:vector) { Vector2d.new(2, 3).normalize }
105
+
106
+ it { is_expected.to be(true) }
107
+ end
108
+
109
+ context "when vector isn't normalized" do
110
+ let(:vector) { Vector2d.new(2, 3) }
111
+
112
+ it { is_expected.to be(false) }
113
+ end
114
+
115
+ context "when vector is barely longer than a unit vector" do
116
+ let(:vector) { Vector2d.new(1 + 1e-9, 0) }
117
+
118
+ it { is_expected.to be(false) }
119
+ end
120
+
121
+ it "is true for any normalized vector" do
122
+ vectors = (1..50).flat_map do |x|
123
+ (1..50).map { |y| Vector2d.new(x, y).normalize }
124
+ end
125
+
126
+ expect(vectors).to all(be_normalized)
127
+ end
128
+ end
129
+
130
+ describe "#normalize" do
131
+ subject { vector.normalize }
132
+
133
+ its(:x) { is_expected.to be_within(0.0001).of(0.5547) }
134
+ its(:y) { is_expected.to be_within(0.0001).of(0.8320) }
135
+
136
+ context "with the zero vector" do
137
+ subject(:vector) { Vector2d.new(0, 0) }
138
+
139
+ it "returns the zero vector" do
140
+ expect(vector.normalize).to eq(Vector2d.new(0, 0))
141
+ end
142
+ end
143
+ end
144
+
145
+ describe "#resize" do
146
+ subject(:resized) { vector.resize(2.0) }
147
+
148
+ it "modifies the vector length" do
149
+ expect(resized.length).to be_within(0.0001).of(2.0)
150
+ end
151
+
152
+ it "modifies the x property" do
153
+ expect(resized.x).to be_within(0.0001).of(1.1094)
154
+ end
155
+
156
+ it "modifies the y property" do
157
+ expect(resized.y).to be_within(0.0001).of(1.6641)
158
+ end
159
+
160
+ context "with the zero vector" do
161
+ subject(:vector) { Vector2d.new(0, 0) }
162
+
163
+ it "returns the zero vector" do
164
+ expect(resized).to eq(Vector2d.new(0, 0))
165
+ end
166
+ end
167
+
168
+ context "with a float zero vector" do
169
+ subject(:vector) { Vector2d.new(0.0, 0.0) }
170
+
171
+ it "returns the zero vector" do
172
+ expect(resized).to eq(Vector2d.new(0.0, 0.0))
173
+ end
174
+ end
175
+
176
+ context "with a complex length" do
177
+ it "raises an error" do
178
+ expect { vector.resize(Complex(1, 2)) }.to(
179
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
180
+ )
181
+ end
182
+ end
183
+
184
+ context "with a complex length and the zero vector" do
185
+ subject(:vector) { Vector2d.new(0, 0) }
186
+
187
+ it "raises an error" do
188
+ expect { vector.resize(Complex(1, 2)) }.to(
189
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
190
+ )
191
+ end
192
+ end
193
+
194
+ context "with a negative length" do
195
+ subject(:resized) { vector.resize(-2.0) }
196
+
197
+ it "modifies the vector length" do
198
+ expect(resized.length).to be_within(0.0001).of(2.0)
199
+ end
200
+
201
+ it "reverses the x property" do
202
+ expect(resized.x).to be_within(0.0001).of(-1.1094)
203
+ end
204
+
205
+ it "reverses the y property" do
206
+ expect(resized.y).to be_within(0.0001).of(-1.6641)
207
+ end
208
+ end
209
+ end
210
+
211
+ describe "#limit_length" do
212
+ context "when argument is longer than length" do
213
+ let(:arg) { 5.0 }
214
+
215
+ it "does not change the length" do
216
+ expect(vector.limit_length(arg).length).to be_within(0.0001).of(3.6055)
217
+ end
218
+ end
219
+
220
+ context "when argument is shorter than length" do
221
+ let(:arg) { 2.5 }
222
+
223
+ it "changes the length" do
224
+ expect(vector.limit_length(arg).length).to be_within(0.0001).of(arg)
225
+ end
226
+ end
227
+
228
+ context "with the zero vector" do
229
+ subject(:vector) { Vector2d.new(0, 0) }
230
+
231
+ it "returns the zero vector" do
232
+ expect(vector.limit_length(5.0)).to eq(Vector2d.new(0, 0))
233
+ end
234
+ end
235
+
236
+ context "when argument is negative" do
237
+ it "raises an error" do
238
+ expect { vector.limit_length(-1.0) }.to(
239
+ raise_error(ArgumentError, "negative max length: -1.0")
240
+ )
241
+ end
242
+ end
243
+
244
+ context "when argument is negative and vector is zero" do
245
+ subject(:vector) { Vector2d.new(0, 0) }
246
+
247
+ it "raises an error" do
248
+ expect { vector.limit_length(-1.0) }.to raise_error(ArgumentError)
249
+ end
250
+ end
251
+
252
+ context "when argument is complex" do
253
+ it "raises an error" do
254
+ expect { vector.limit_length(Complex(1, 2)) }.to(
255
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
256
+ )
257
+ end
258
+ end
259
+
260
+ context "when argument is nil" do
261
+ it "raises an error" do
262
+ expect { vector.limit_length(nil) }.to(
263
+ raise_error(ArgumentError, "not a valid coordinate: nil")
264
+ )
265
+ end
266
+ end
267
+ end
268
+
269
+ describe "#clamp_length" do
270
+ it "raises an error without a maximum" do
271
+ expect { vector.clamp_length(2.5) }.to(
272
+ raise_error(ArgumentError,
273
+ "wrong number of arguments (given 1, expected 2)")
274
+ )
275
+ end
276
+
277
+ context "with a minimum and a maximum" do
278
+ it "scales a short vector up to the minimum" do
279
+ expect(vector.clamp_length(5.0, 10.0).length)
280
+ .to be_within(0.0001).of(5.0)
281
+ end
282
+
283
+ it "scales a long vector down to the maximum" do
284
+ expect(vector.clamp_length(0.5, 1.0).length)
285
+ .to be_within(0.0001).of(1.0)
286
+ end
287
+
288
+ it "leaves a vector between the bounds alone" do
289
+ expect(vector.clamp_length(1.0, 10.0).length)
290
+ .to be_within(0.0001).of(3.6055)
291
+ end
292
+
293
+ it "matches #limit_length when the minimum is zero" do
294
+ expect(vector.clamp_length(0, 2.5)).to eq(vector.limit_length(2.5))
295
+ end
296
+
297
+ it "raises an error when the minimum is negative" do
298
+ expect { vector.clamp_length(-1.0, 10.0) }.to(
299
+ raise_error(ArgumentError, "negative min length: -1.0")
300
+ )
301
+ end
302
+
303
+ it "raises an error when the minimum exceeds the maximum" do
304
+ expect { vector.clamp_length(10.0, 1.0) }.to raise_error(ArgumentError)
305
+ end
306
+
307
+ it "leaves the zero vector alone" do
308
+ expect(Vector2d.new(0, 0).clamp_length(5.0, 10.0))
309
+ .to eq(Vector2d.new(0, 0))
310
+ end
311
+ end
312
+
313
+ context "with a range" do
314
+ it "clamps the length between the bounds" do
315
+ expect(vector.clamp_length(5.0..10.0).length)
316
+ .to be_within(0.0001).of(5.0)
317
+ end
318
+
319
+ it "matches the two argument form" do
320
+ expect(vector.clamp_length(5.0..10.0))
321
+ .to eq(vector.clamp_length(5.0, 10.0))
322
+ end
323
+
324
+ it "clamps only the upper bound of a beginless range" do
325
+ expect(vector.clamp_length(..1.0).length).to be_within(0.0001).of(1.0)
326
+ end
327
+
328
+ it "clamps only the lower bound of an endless range" do
329
+ expect(vector.clamp_length(5.0..).length).to be_within(0.0001).of(5.0)
330
+ end
331
+
332
+ it "matches #limit_length for a beginless range" do
333
+ expect(vector.clamp_length(..1.0)).to eq(vector.limit_length(1.0))
334
+ end
335
+
336
+ it "raises an error with an exclusive range" do
337
+ expect { vector.clamp_length(5.0...10.0) }.to(
338
+ raise_error(ArgumentError, "cannot clamp with an exclusive range")
339
+ )
340
+ end
341
+
342
+ it "raises an error with a negative bound" do
343
+ expect { vector.clamp_length(-1.0..10.0) }.to(
344
+ raise_error(ArgumentError, "negative min length: -1.0")
345
+ )
346
+ end
347
+
348
+ it "raises an error with a second argument" do
349
+ expect { vector.clamp_length(5.0..10.0, 20.0) }.to(
350
+ raise_error(ArgumentError,
351
+ "wrong number of arguments (given 2, expected 1)")
352
+ )
353
+ end
354
+ end
355
+ end
356
+
357
+ describe "#distance" do
358
+ let(:comp) { Vector2d.new(3, 4) }
359
+
360
+ it "returns the distance between two vectors" do
361
+ expect(vector.distance(comp)).to be_within(0.0001).of(1.4142)
362
+ end
363
+ end
364
+
365
+ describe "#distance_squared" do
366
+ let(:comp) { Vector2d.new(5, 6) }
367
+
368
+ it "returns the squared distance between two vectors" do
369
+ expect(vector.distance_squared(comp)).to eq(18)
370
+ end
371
+ end
372
+
373
+ describe "#squared_distance" do
374
+ subject(:vector) { Vector2d.new(2, 3).squared_distance(Vector2d.new(5, 6)) }
375
+
376
+ it_behaves_like "a deprecated method", "squared_distance", "#distance_squared"
377
+
378
+ it "returns the squared distance, as #distance_squared does" do
379
+ expect(vector).to eq(18)
380
+ end
381
+ end
382
+
383
+ describe "#manhattan_distance" do
384
+ let(:comp) { Vector2d.new(5, 7) }
385
+
386
+ it "returns the sum of the absolute differences" do
387
+ expect(vector.manhattan_distance(comp)).to eq(7)
388
+ end
389
+
390
+ it "is symmetric" do
391
+ expect(comp.manhattan_distance(vector))
392
+ .to eq(vector.manhattan_distance(comp))
393
+ end
394
+
395
+ it "returns zero for the same vector" do
396
+ expect(vector.manhattan_distance(vector)).to eq(0)
397
+ end
398
+
399
+ it "coerces the argument" do
400
+ expect(vector.manhattan_distance([5, 7])).to eq(7)
401
+ end
402
+ end
403
+
404
+ describe "#chebyshev_distance" do
405
+ let(:comp) { Vector2d.new(5, 7) }
406
+
407
+ it "returns the largest absolute difference" do
408
+ expect(vector.chebyshev_distance(comp)).to eq(4)
409
+ end
410
+
411
+ it "is symmetric" do
412
+ expect(comp.chebyshev_distance(vector))
413
+ .to eq(vector.chebyshev_distance(comp))
414
+ end
415
+
416
+ it "returns zero for the same vector" do
417
+ expect(vector.chebyshev_distance(vector)).to eq(0)
418
+ end
419
+
420
+ it "coerces the argument" do
421
+ expect(vector.chebyshev_distance([5, 7])).to eq(4)
422
+ end
423
+ end
424
+
425
+ describe "#direction_to" do
426
+ subject(:vector) { Vector2d.new(2, 3) }
427
+
428
+ let(:comp) { Vector2d.new(5, 7) }
429
+
430
+ it "points at the other vector" do
431
+ expect(vector.direction_to(comp).round(3)).to eq(Vector2d.new(0.6, 0.8))
432
+ end
433
+
434
+ it "returns a unit vector" do
435
+ expect(vector.direction_to(comp).length).to be_within(0.0001).of(1.0)
436
+ end
437
+
438
+ it "matches the normalized difference" do
439
+ expect(vector.direction_to(comp)).to eq((comp - vector).normalize)
440
+ end
441
+
442
+ it "points the other way around" do
443
+ expect(vector.direction_to(comp))
444
+ .to eq(comp.direction_to(vector).reverse)
445
+ end
446
+
447
+ it "coerces the argument" do
448
+ expect(vector.direction_to([5, 7])).to eq(vector.direction_to(comp))
449
+ end
450
+
451
+ context "when the other vector is this one" do
452
+ it "returns the zero vector" do
453
+ expect(vector.direction_to(vector)).to eq(Vector2d.new(0, 0))
454
+ end
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "matrix"
5
+
6
+ describe Vector2d::MatrixInterop do
7
+ subject(:vector) { Vector2d.new(2, 3) }
8
+
9
+ let(:rotation) { Matrix[[0, -1], [1, 0]] }
10
+
11
+ describe ".matrix?" do
12
+ it "recognizes a matrix" do
13
+ expect(described_class).to be_matrix(rotation)
14
+ end
15
+
16
+ it "doesn't recognize anything else" do
17
+ expect(described_class).not_to be_matrix([[0, -1], [1, 0]])
18
+ end
19
+ end
20
+
21
+ describe ".vector?" do
22
+ it "recognizes a vector" do
23
+ expect(described_class).to be_vector(Vector[2, 3])
24
+ end
25
+
26
+ it "doesn't recognize anything else" do
27
+ expect(described_class).not_to be_vector(vector)
28
+ end
29
+ end
30
+
31
+ describe "#coerce" do
32
+ it "returns the matrix first, followed by itself as a vector" do
33
+ expect(vector.coerce(rotation)).to eq([rotation, Vector[2, 3]])
34
+ end
35
+
36
+ it "makes a vector the right hand operand of a matrix" do
37
+ expect(rotation * vector).to eq(Vector[-3, 2])
38
+ end
39
+
40
+ it "makes a vector the left hand operand of a Vector" do
41
+ expect(Vector[1, 2] + vector).to eq(Vector2d.new(3, 5))
42
+ end
43
+
44
+ context "when the other operand isn't a matrix" do
45
+ it "falls back to parsing the operand" do
46
+ expect(vector.coerce(4)).to eq([Vector2d.new(4, 4), vector])
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "#to_matrix" do
52
+ it "returns a column matrix" do
53
+ expect(vector.to_matrix).to eq(Matrix[[2], [3]])
54
+ end
55
+ end
56
+
57
+ describe "#to_vector" do
58
+ it "returns a vector" do
59
+ expect(vector.to_vector).to eq(Vector[2, 3])
60
+ end
61
+ end
62
+
63
+ describe "#transform" do
64
+ it "multiplies the matrix by itself as a column" do
65
+ expect(vector.transform(rotation)).to eq(Vector2d.new(-3, 2))
66
+ end
67
+
68
+ context "when the argument isn't a matrix" do
69
+ it "raises a TypeError" do
70
+ expect { vector.transform([[0, -1], [1, 0]]) }.to(
71
+ raise_error(TypeError, "Array is not a Matrix")
72
+ )
73
+ end
74
+ end
75
+
76
+ context "when the matrix doesn't have two columns" do
77
+ it "raises a dimension mismatch" do
78
+ expect { vector.transform(Matrix[[1, 2, 3], [4, 5, 6]]) }.to(
79
+ raise_error(ExceptionForMatrix::ErrDimensionMismatch)
80
+ )
81
+ end
82
+ end
83
+
84
+ context "when the matrix holds a complex" do
85
+ it "raises an error" do
86
+ expect { vector.transform(Matrix[[Complex(0, 1), 0], [0, 1]]) }.to(
87
+ raise_error(ArgumentError, "not a valid coordinate: (0+2i)")
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end