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