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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +39 -0
  3. data/.github/workflows/build.yml +48 -10
  4. data/.gitignore +3 -1
  5. data/.release-please-manifest.json +3 -0
  6. data/.rubocop.yml +5 -2
  7. data/.tool-versions +1 -0
  8. data/.yardopts +11 -0
  9. data/CHANGELOG.md +224 -0
  10. data/CODE_OF_CONDUCT.md +92 -0
  11. data/CONTRIBUTING.md +30 -0
  12. data/Gemfile +6 -2
  13. data/Gemfile.lock +85 -51
  14. data/README.md +330 -13
  15. data/Rakefile +21 -0
  16. data/benchmark/vector_comparison.rb +129 -0
  17. data/lib/vector2d/angles.rb +315 -0
  18. data/lib/vector2d/arithmetic.rb +97 -0
  19. data/lib/vector2d/comparison.rb +188 -0
  20. data/lib/vector2d/componentwise.rb +239 -0
  21. data/lib/vector2d/constructors.rb +137 -0
  22. data/lib/vector2d/conversions.rb +139 -0
  23. data/lib/vector2d/coordinates.rb +22 -0
  24. data/lib/vector2d/deprecation.rb +16 -0
  25. data/lib/vector2d/dimensions.rb +297 -0
  26. data/lib/vector2d/interpolation.rb +191 -0
  27. data/lib/vector2d/lengths.rb +284 -0
  28. data/lib/vector2d/matrix_interop.rb +93 -0
  29. data/lib/vector2d/parsing.rb +142 -0
  30. data/lib/vector2d/projection.rb +208 -0
  31. data/lib/vector2d/version.rb +2 -1
  32. data/lib/vector2d.rb +174 -62
  33. data/release-please-config.json +10 -0
  34. data/spec/lib/vector2d/angles_spec.rb +441 -0
  35. data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
  36. data/spec/lib/vector2d/comparison_spec.rb +358 -0
  37. data/spec/lib/vector2d/componentwise_spec.rb +300 -0
  38. data/spec/lib/vector2d/constructors_spec.rb +299 -0
  39. data/spec/lib/vector2d/conversions_spec.rb +234 -0
  40. data/spec/lib/vector2d/dimensions_spec.rb +820 -0
  41. data/spec/lib/vector2d/interpolation_spec.rb +322 -0
  42. data/spec/lib/vector2d/lengths_spec.rb +457 -0
  43. data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
  44. data/spec/lib/vector2d/parsing_spec.rb +329 -0
  45. data/spec/lib/vector2d/projection_spec.rb +306 -0
  46. data/spec/lib/vector2d_documentation_spec.rb +38 -0
  47. data/spec/lib/vector2d_immutability_spec.rb +263 -0
  48. data/spec/lib/vector2d_spec.rb +90 -50
  49. data/spec/lib/vector2d_subclassing_spec.rb +210 -0
  50. data/spec/lib/vector2d_tags_spec.rb +43 -0
  51. data/spec/spec_helper.rb +2 -0
  52. data/spec/support/doc_examples/comments.rb +65 -0
  53. data/spec/support/doc_examples/markdown.rb +50 -0
  54. data/spec/support/doc_examples.rb +94 -0
  55. data/spec/support/doc_tags.rb +151 -0
  56. data/spec/support/shared_examples/class_preserving_method.rb +10 -0
  57. data/spec/support/shared_examples/deprecated_method.rb +25 -0
  58. data/spec/support/shared_examples/parsed_vector.rb +11 -0
  59. data/vector2d.gemspec +9 -3
  60. metadata +55 -19
  61. data/.travis.yml +0 -10
  62. data/lib/vector2d/calculations.rb +0 -141
  63. data/lib/vector2d/coercions.rb +0 -64
  64. data/lib/vector2d/fitting.rb +0 -60
  65. data/lib/vector2d/properties.rb +0 -46
  66. data/lib/vector2d/transformations.rb +0 -85
  67. data/spec/lib/vector2d/coercions_spec.rb +0 -59
  68. data/spec/lib/vector2d/fitting_spec.rb +0 -70
  69. data/spec/lib/vector2d/properties_spec.rb +0 -47
  70. data/spec/lib/vector2d/transformations_spec.rb +0 -122
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "matrix"
5
+
6
+ describe Vector2d::Parsing do
7
+ describe ".parse" do
8
+ context "with fixnum argument" do
9
+ subject(:vector) { Vector2d.parse(2) }
10
+
11
+ it_behaves_like "a parsed vector", [2, 2]
12
+ end
13
+
14
+ context "with two arguments, fixnum" do
15
+ subject(:vector) { Vector2d.parse(1, 2) }
16
+
17
+ it_behaves_like "a parsed vector", [1, 2]
18
+ end
19
+
20
+ context "with two arguments, float" do
21
+ subject(:vector) { Vector2d.parse(1.0, 2.0) }
22
+
23
+ it_behaves_like "a parsed vector", [1.0, 2.0]
24
+ end
25
+
26
+ context "with string argument, first omitted" do
27
+ subject(:vector) { Vector2d.parse("x200") }
28
+
29
+ it_behaves_like "a parsed vector", [0, 200]
30
+ end
31
+
32
+ context "with string argument, second omitted" do
33
+ subject(:vector) { Vector2d.parse("200x") }
34
+
35
+ it_behaves_like "a parsed vector", [200, 0]
36
+ end
37
+
38
+ context "with string argument, fixnum" do
39
+ subject(:vector) { Vector2d.parse("1x2") }
40
+
41
+ it_behaves_like "a parsed vector", [1, 2]
42
+ end
43
+
44
+ context "with string argument, float" do
45
+ subject(:vector) { Vector2d.parse("1.0x2.0") }
46
+
47
+ it_behaves_like "a parsed vector", [1.0, 2.0]
48
+ end
49
+
50
+ context "with string argument, negative" do
51
+ subject(:vector) { Vector2d.parse("-1x-2") }
52
+
53
+ it_behaves_like "a parsed vector", [-1, -2]
54
+ end
55
+
56
+ context "with string argument, explicitly positive" do
57
+ subject(:vector) { Vector2d.parse("+1x+2") }
58
+
59
+ it_behaves_like "a parsed vector", [1, 2]
60
+ end
61
+
62
+ context "with string argument, no leading zero" do
63
+ subject(:vector) { Vector2d.parse(".5x.25") }
64
+
65
+ it_behaves_like "a parsed vector", [0.5, 0.25]
66
+ end
67
+
68
+ context "with string argument, uppercase separator" do
69
+ subject(:vector) { Vector2d.parse("1X2") }
70
+
71
+ it_behaves_like "a parsed vector", [1, 2]
72
+ end
73
+
74
+ context "with string argument, comma separator" do
75
+ subject(:vector) { Vector2d.parse("1, 2") }
76
+
77
+ it_behaves_like "a parsed vector", [1, 2]
78
+ end
79
+
80
+ context "with string argument, surrounding whitespace" do
81
+ subject(:vector) { Vector2d.parse(" 1 x 2 ") }
82
+
83
+ it_behaves_like "a parsed vector", [1, 2]
84
+ end
85
+
86
+ context "with string argument, mixing integer and float" do
87
+ subject(:vector) { Vector2d.parse("1x2.0") }
88
+
89
+ its(:x) { is_expected.to be_an(Integer) }
90
+ its(:y) { is_expected.to be_a(Float) }
91
+ end
92
+
93
+ context "with a malformed string argument" do
94
+ it "raises an error" do
95
+ expect { Vector2d.parse("1.2.3x4") }.to(
96
+ raise_error(ArgumentError, 'not a valid string input: "1.2.3x4"')
97
+ )
98
+ end
99
+ end
100
+
101
+ context "with a string argument holding three coordinates" do
102
+ it "raises an error" do
103
+ expect { Vector2d.parse("1x2x3") }.to(
104
+ raise_error(ArgumentError, 'not a valid string input: "1x2x3"')
105
+ )
106
+ end
107
+ end
108
+
109
+ context "with a string argument holding no coordinates" do
110
+ it "raises an error" do
111
+ expect { Vector2d.parse("foo") }.to(
112
+ raise_error(ArgumentError, 'not a valid string input: "foo"')
113
+ )
114
+ end
115
+ end
116
+
117
+ context "with array argument" do
118
+ subject(:vector) { Vector2d.parse([1, 2]) }
119
+
120
+ it_behaves_like "a parsed vector", [1, 2]
121
+ end
122
+
123
+ context "with single element array argument" do
124
+ subject(:vector) { Vector2d.parse([5]) }
125
+
126
+ it_behaves_like "a parsed vector", [5, 5]
127
+ end
128
+
129
+ context "with hash argument, symbol keys" do
130
+ subject(:vector) { Vector2d.parse(x: 1, y: 2) }
131
+
132
+ it_behaves_like "a parsed vector", [1, 2]
133
+ end
134
+
135
+ context "with hash argument, string keys" do
136
+ subject(:vector) { Vector2d.parse("x" => 1, "y" => 2) }
137
+
138
+ it_behaves_like "a parsed vector", [1, 2]
139
+ end
140
+
141
+ context "with vector argument" do
142
+ subject(:vector) { Vector2d.parse(Vector2d.new(1, 2)) }
143
+
144
+ it_behaves_like "a parsed vector", [1, 2]
145
+ end
146
+
147
+ context "with a Vector argument" do
148
+ subject(:vector) { Vector2d.parse(Vector[1, 2]) }
149
+
150
+ it_behaves_like "a parsed vector", [1, 2]
151
+ end
152
+
153
+ context "with a Vector argument of the wrong size" do
154
+ it "raises an error" do
155
+ expect { Vector2d.parse(Vector[1, 2, 3]) }.to(
156
+ raise_error(ArgumentError, "expected 2 coordinates, got 3")
157
+ )
158
+ end
159
+ end
160
+
161
+ context "with a column Matrix argument" do
162
+ subject(:vector) { Vector2d.parse(Matrix[[1], [2]]) }
163
+
164
+ it_behaves_like "a parsed vector", [1, 2]
165
+ end
166
+
167
+ context "with a row Matrix argument" do
168
+ subject(:vector) { Vector2d.parse(Matrix[[1, 2]]) }
169
+
170
+ it_behaves_like "a parsed vector", [1, 2]
171
+ end
172
+
173
+ context "with a Matrix argument of the wrong shape" do
174
+ it "raises an error" do
175
+ expect { Vector2d.parse(Matrix[[1, 2], [3, 4]]) }.to(
176
+ raise_error(ArgumentError, "expected a 2x1 or 1x2 matrix, got 2x2")
177
+ )
178
+ end
179
+ end
180
+
181
+ context "with a Vector argument holding a complex" do
182
+ it "raises an error" do
183
+ expect { Vector2d.parse(Vector[Complex(1, 2), 3]) }.to(
184
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
185
+ )
186
+ end
187
+ end
188
+
189
+ context "with a Matrix argument holding a complex" do
190
+ it "raises an error" do
191
+ expect { Vector2d.parse(Matrix[[Complex(1, 2)], [3]]) }.to(
192
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
193
+ )
194
+ end
195
+ end
196
+
197
+ context "with nil argument" do
198
+ it "raises an error" do
199
+ expect { Vector2d.parse(nil) }.to(
200
+ raise_error(ArgumentError, "not a valid coordinate: nil")
201
+ )
202
+ end
203
+ end
204
+
205
+ context "with a non-numeric argument" do
206
+ it "raises an error" do
207
+ expect { Vector2d.parse(:foo) }.to(
208
+ raise_error(ArgumentError, "not a valid coordinate: :foo")
209
+ )
210
+ end
211
+ end
212
+
213
+ context "with rational argument" do
214
+ subject(:vector) { Vector2d.parse(Rational(1, 2), 3) }
215
+
216
+ it_behaves_like "a parsed vector", [Rational(1, 2), 3]
217
+ end
218
+
219
+ context "with complex argument" do
220
+ it "raises an error" do
221
+ expect { Vector2d.parse(Complex(1, 2)) }.to(
222
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
223
+ )
224
+ end
225
+ end
226
+
227
+ context "with a complex coordinate among two arguments" do
228
+ it "raises an error" do
229
+ expect { Vector2d.parse(Complex(1, 2), 3) }.to(
230
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
231
+ )
232
+ end
233
+ end
234
+
235
+ context "with complex argument that has a zero imaginary part" do
236
+ it "raises an error" do
237
+ expect { Vector2d.parse(Complex(1, 0)) }.to(
238
+ raise_error(ArgumentError, "not a valid coordinate: (1+0i)")
239
+ )
240
+ end
241
+ end
242
+
243
+ context "with array argument holding a complex" do
244
+ it "raises an error" do
245
+ expect { Vector2d.parse([Complex(1, 2), 3]) }.to(
246
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
247
+ )
248
+ end
249
+ end
250
+
251
+ context "with hash argument holding a complex" do
252
+ it "raises an error" do
253
+ expect { Vector2d.parse(x: Complex(1, 2), y: 3) }.to(
254
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
255
+ )
256
+ end
257
+ end
258
+
259
+ context "with a non-numeric second argument" do
260
+ it "raises an error" do
261
+ expect { Vector2d.parse(1, "2") }.to(
262
+ raise_error(ArgumentError, 'not a valid coordinate: "2"')
263
+ )
264
+ end
265
+ end
266
+
267
+ context "with nil as the second argument" do
268
+ it "raises an error" do
269
+ expect { Vector2d.parse(5, nil) }.to(
270
+ raise_error(ArgumentError, "not a valid coordinate: nil")
271
+ )
272
+ end
273
+ end
274
+
275
+ context "with nil as the second argument of the global method" do
276
+ it "raises an error" do
277
+ expect { Vector2d(5, nil) }.to(
278
+ raise_error(ArgumentError, "not a valid coordinate: nil")
279
+ )
280
+ end
281
+ end
282
+
283
+ context "with empty hash argument" do
284
+ it "raises an error" do
285
+ expect { Vector2d.parse({}) }.to(
286
+ raise_error(ArgumentError, "not a valid coordinate: nil")
287
+ )
288
+ end
289
+ end
290
+
291
+ context "with hash argument missing y" do
292
+ it "raises an error" do
293
+ expect { Vector2d.parse(x: 1) }.to(
294
+ raise_error(ArgumentError, "not a valid coordinate: nil")
295
+ )
296
+ end
297
+ end
298
+
299
+ context "with hash argument holding a non-numeric value" do
300
+ it "raises an error" do
301
+ expect { Vector2d.parse(x: 1, y: "2") }.to(
302
+ raise_error(ArgumentError, 'not a valid coordinate: "2"')
303
+ )
304
+ end
305
+ end
306
+
307
+ context "with array argument holding nil" do
308
+ it "raises an error" do
309
+ expect { Vector2d.parse([1, nil]) }.to(
310
+ raise_error(ArgumentError, "not a valid coordinate: nil")
311
+ )
312
+ end
313
+ end
314
+
315
+ context "with array argument of the wrong size" do
316
+ it "raises an error" do
317
+ expect { Vector2d.parse([1, 2, 3]) }.to(
318
+ raise_error(ArgumentError, "expected 1 or 2 coordinates, got 3")
319
+ )
320
+ end
321
+ end
322
+
323
+ context "with hash argument, nil symbol key falling back to string key" do
324
+ subject(:vector) { Vector2d.parse(x: nil, "x" => 1, y: 2) }
325
+
326
+ it_behaves_like "a parsed vector", [1, 2]
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Vector2d::Projection do
6
+ subject(:vector) { Vector2d.new(2, 3) }
7
+
8
+ describe ".dot_product" do
9
+ let(:v1) { Vector2d.new(2, 3) }
10
+ let(:v2) { Vector2d.new(4, 5) }
11
+
12
+ it "calculates the dot product between two vectors" do
13
+ expect(Vector2d.dot_product(v1, v2)).to eq(23)
14
+ end
15
+ end
16
+
17
+ describe ".cross_product" do
18
+ let(:v1) { Vector2d.new(2, 3) }
19
+ let(:v2) { Vector2d.new(4, 5) }
20
+
21
+ it "calculates the cross product between two vectors" do
22
+ expect(Vector2d.cross_product(v1, v2)).to eq(-2.0)
23
+ end
24
+ end
25
+
26
+ describe "#dot_product" do
27
+ let(:comp) { Vector2d.new(3, 4) }
28
+
29
+ it "calulates the dot product" do
30
+ expect(vector.dot_product(comp)).to eq(Vector2d.dot_product(vector, comp))
31
+ end
32
+ end
33
+
34
+ describe "#inner_product" do
35
+ let(:comp) { Vector2d.new(3, 4) }
36
+
37
+ it "is an alias of #dot_product" do
38
+ expect(vector.inner_product(comp)).to eq(vector.dot_product(comp))
39
+ end
40
+ end
41
+
42
+ describe "#dot" do
43
+ let(:comp) { Vector2d.new(3, 4) }
44
+
45
+ it "is an alias of #dot_product" do
46
+ expect(vector.dot(comp)).to eq(vector.dot_product(comp))
47
+ end
48
+ end
49
+
50
+ describe "#cross_product" do
51
+ let(:comp) { Vector2d.new(3, 4) }
52
+
53
+ it "calulates the cross product" do
54
+ expect(
55
+ vector.cross_product(comp)
56
+ ).to eq(Vector2d.cross_product(vector, comp))
57
+ end
58
+ end
59
+
60
+ describe "#project" do
61
+ let(:comp) { Vector2d.new(4, 0) }
62
+
63
+ it "returns the component along the other vector" do
64
+ expect(vector.project(comp)).to eq(Vector2d.new(2.0, 0.0))
65
+ end
66
+
67
+ it "ignores the magnitude of the other vector" do
68
+ expect(vector.project(comp * 10)).to eq(vector.project(comp))
69
+ end
70
+
71
+ it "ignores the direction of the other vector" do
72
+ expect(vector.project(comp.reverse)).to eq(vector.project(comp))
73
+ end
74
+
75
+ it "is parallel to the other vector" do
76
+ expect(vector.project(comp).angle_between(comp)).to eq(0.0)
77
+ end
78
+
79
+ it "coerces the argument" do
80
+ expect(vector.project([4, 0])).to eq(vector.project(comp))
81
+ end
82
+
83
+ it "returns the zero vector when projecting onto a zero vector" do
84
+ expect(vector.project(Vector2d.new(0, 0))).to eq(Vector2d.new(0, 0))
85
+ end
86
+
87
+ it "returns the zero vector when projecting a zero vector" do
88
+ expect(Vector2d.new(0, 0).project(comp)).to eq(Vector2d.new(0, 0))
89
+ end
90
+
91
+ describe "the components" do
92
+ subject { vector.project(comp) }
93
+
94
+ its(:x) { is_expected.to be_a(Float) }
95
+ its(:y) { is_expected.to be_a(Float) }
96
+ end
97
+
98
+ describe "the components of a projection onto a zero vector" do
99
+ subject { vector.project(Vector2d.new(0, 0)) }
100
+
101
+ its(:x) { is_expected.to be_a(Float) }
102
+ its(:y) { is_expected.to be_a(Float) }
103
+ end
104
+ end
105
+
106
+ describe "#reject" do
107
+ let(:comp) { Vector2d.new(4, 0) }
108
+
109
+ it "returns the component perpendicular to the other vector" do
110
+ expect(vector.reject(comp)).to eq(Vector2d.new(0.0, 3.0))
111
+ end
112
+
113
+ it "is what remains when the projection is subtracted" do
114
+ expect(vector.project(comp) + vector.reject(comp)).to eq(vector)
115
+ end
116
+
117
+ it "coerces the argument" do
118
+ expect(vector.reject([4, 0])).to eq(vector.reject(comp))
119
+ end
120
+
121
+ it "returns the vector unchanged for a zero vector" do
122
+ expect(vector.reject(Vector2d.new(0, 0))).to eq(vector)
123
+ end
124
+ end
125
+
126
+ describe "#scalar_projection" do
127
+ let(:comp) { Vector2d.new(4, 0) }
128
+
129
+ it "returns the signed length of the projection" do
130
+ expect(vector.scalar_projection(comp)).to eq(2.0)
131
+ end
132
+
133
+ it "is negative when the vectors point in opposite directions" do
134
+ expect(vector.scalar_projection(comp.reverse)).to eq(-2.0)
135
+ end
136
+
137
+ it "ignores the magnitude of the other vector" do
138
+ expect(vector.scalar_projection(comp * 10)).to eq(2.0)
139
+ end
140
+
141
+ it "matches the length of the vector projection" do
142
+ expect(vector.scalar_projection(comp).abs)
143
+ .to be_within(1e-12).of(vector.project(comp).length)
144
+ end
145
+
146
+ it "coerces the argument" do
147
+ expect(vector.scalar_projection([4, 0])).to eq(2.0)
148
+ end
149
+
150
+ it "returns zero for a zero vector" do
151
+ expect(vector.scalar_projection(Vector2d.new(0, 0))).to eq(0.0)
152
+ end
153
+ end
154
+
155
+ describe "#reflect" do
156
+ let(:normal) { Vector2d.new(1, 2) }
157
+
158
+ it "reflects the vector about the line perpendicular to the normal" do
159
+ expect(vector.reflect(Vector2d.new(0, 1))).to eq(Vector2d.new(2.0, -3.0))
160
+ end
161
+
162
+ it "normalizes the normal" do
163
+ expect(vector.reflect(Vector2d.new(0, 5)))
164
+ .to eq(vector.reflect(Vector2d.new(0, 1)))
165
+ end
166
+
167
+ it "reverses a vector parallel to the normal" do
168
+ expect(vector.reflect(vector).distance(vector.reverse))
169
+ .to be_within(1e-12).of(0.0)
170
+ end
171
+
172
+ it "preserves the length of the vector" do
173
+ expect(vector.reflect(normal).length).to be_within(1e-12).of(vector.length)
174
+ end
175
+
176
+ it "returns the original vector when applied twice" do
177
+ expect(vector.reflect(normal).reflect(normal).distance(vector))
178
+ .to be_within(1e-12).of(0.0)
179
+ end
180
+
181
+ it "coerces the argument" do
182
+ expect(vector.reflect([0, 1])).to eq(vector.reflect(Vector2d.new(0, 1)))
183
+ end
184
+
185
+ it "returns the vector unchanged for a zero vector" do
186
+ expect(vector.reflect(Vector2d.new(0, 0))).to eq(vector)
187
+ end
188
+
189
+ it "ignores the component types of a zero normal" do
190
+ expect(vector.reflect(Vector2d.new(0, 0)))
191
+ .to eql(vector.reflect(Vector2d.new(0.0, 0.0)))
192
+ end
193
+
194
+ describe "the components" do
195
+ subject { vector.reflect(normal) }
196
+
197
+ its(:x) { is_expected.to be_a(Float) }
198
+ its(:y) { is_expected.to be_a(Float) }
199
+ end
200
+
201
+ describe "the components of a reflection off a zero vector" do
202
+ subject { vector.reflect(Vector2d.new(0, 0)) }
203
+
204
+ its(:x) { is_expected.to be_a(Float) }
205
+ its(:y) { is_expected.to be_a(Float) }
206
+ end
207
+ end
208
+
209
+ describe "#refract" do
210
+ subject(:vector) { Vector2d.new(1, -1).normalize }
211
+
212
+ let(:normal) { Vector2d.new(0, 1) }
213
+
214
+ it "bends the vector towards the normal" do
215
+ expect(vector.refract(normal, 0.5))
216
+ .to eq(Vector2d.new(0.35355339059327373, -0.9354143466934853))
217
+ end
218
+
219
+ it "normalizes the normal" do
220
+ expect(vector.refract(Vector2d.new(0, 5), 0.5))
221
+ .to eq(vector.refract(normal, 0.5))
222
+ end
223
+
224
+ it "leaves the vector on course for a ratio of one" do
225
+ expect(vector.refract(normal, 1.0).distance(vector))
226
+ .to be_within(1e-12).of(0.0)
227
+ end
228
+
229
+ it "preserves the length of a unit vector" do
230
+ expect(vector.refract(normal, 0.5).length).to be_within(1e-12).of(1.0)
231
+ end
232
+
233
+ it "keeps the vector on the far side of the surface" do
234
+ expect(vector.refract(normal, 0.5).dot_product(normal)).to be_negative
235
+ end
236
+
237
+ it "coerces the normal" do
238
+ expect(vector.refract([0, 1], 0.5)).to eq(vector.refract(normal, 0.5))
239
+ end
240
+
241
+ context "when the ratio is an integer" do
242
+ it "refracts as with the equivalent float" do
243
+ expect(vector.refract(normal, 1)).to eq(vector.refract(normal, 1.0))
244
+ end
245
+ end
246
+
247
+ context "when past the critical angle" do
248
+ subject { vector.refract(normal, 2.0) }
249
+
250
+ it "returns the zero vector" do
251
+ expect(vector.refract(normal, 2.0)).to eq(Vector2d.new(0.0, 0.0))
252
+ end
253
+
254
+ its(:x) { is_expected.to be_a(Float) }
255
+ its(:y) { is_expected.to be_a(Float) }
256
+ end
257
+
258
+ context "with a grazing angle" do
259
+ subject(:vector) { Vector2d.new(1, -0.0001).normalize }
260
+
261
+ it "refracts without reaching total internal reflection" do
262
+ expect(vector.refract(normal, 0.5).length).to be_within(1e-12).of(1.0)
263
+ end
264
+ end
265
+
266
+ context "with a zero normal" do
267
+ it "returns the vector unchanged" do
268
+ expect(vector.refract(Vector2d.new(0, 0), 0.5)).to eq(vector)
269
+ end
270
+
271
+ it "ignores the component types of the normal" do
272
+ expect(vector.refract(Vector2d.new(0, 0), 0.5))
273
+ .to eql(vector.refract(Vector2d.new(0.0, 0.0), 0.5))
274
+ end
275
+
276
+ it "raises before checking the normal for a complex ratio" do
277
+ expect { vector.refract(Vector2d.new(0, 0), Complex(1, 2)) }.to(
278
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
279
+ )
280
+ end
281
+ end
282
+
283
+ context "with integer coordinates" do
284
+ subject { Vector2d.new(0, -1).refract(normal, 0.5) }
285
+
286
+ its(:x) { is_expected.to be_a(Float) }
287
+ its(:y) { is_expected.to be_a(Float) }
288
+ end
289
+
290
+ context "with a complex ratio" do
291
+ it "raises an error" do
292
+ expect { vector.refract(normal, Complex(1, 2)) }.to(
293
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
294
+ )
295
+ end
296
+ end
297
+
298
+ context "with a vector ratio" do
299
+ it "raises an error" do
300
+ expect { vector.refract(normal, Vector2d.new(1, 2)) }.to(
301
+ raise_error(ArgumentError, "not a valid coordinate: Vector2d(1,2)")
302
+ )
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "matrix"
5
+
6
+ describe Vector2d do
7
+ # Examples are evaluated with +self+ set to a vector, so the ones
8
+ # documenting private instance methods need no receiver. Each block
9
+ # gets its own namespace, so the classes they define stay local.
10
+ def new_scope
11
+ Module.new.module_eval(
12
+ "Vector2d.new(0, 0).instance_eval { binding }", __FILE__, __LINE__
13
+ )
14
+ end
15
+
16
+ def failure_message(doc) = "#{doc.location}: #{doc.code}"
17
+
18
+ def expect_documented(scope, doc)
19
+ return scope.eval(doc.code) unless doc.expectation?
20
+ return expect_raise(scope, doc) if doc.raises?
21
+
22
+ expect(doc.inspected(scope.eval(doc.code)))
23
+ .to match(doc.pattern), failure_message(doc)
24
+ end
25
+
26
+ def expect_raise(scope, doc)
27
+ expect { scope.eval(doc.code) }
28
+ .to raise_error(doc.exception), failure_message(doc)
29
+ end
30
+
31
+ DocExamples.blocks.each do |block|
32
+ it "matches the output shown for #{block.name} " \
33
+ "(#{block.file}:#{block.line})", :aggregate_failures do
34
+ scope = new_scope
35
+ block.examples.each { |doc| expect_documented(scope, doc) }
36
+ end
37
+ end
38
+ end