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,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
|
|
6
|
+
describe Vector2d::Angles do
|
|
7
|
+
subject(:vector) { Vector2d.new(2, 3) }
|
|
8
|
+
|
|
9
|
+
describe ".radians" do
|
|
10
|
+
it "converts degrees to radians" do
|
|
11
|
+
expect(Vector2d.radians(90)).to be_within(0.0001).of(Math::PI / 2)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "converts negative angles" do
|
|
15
|
+
expect(Vector2d.radians(-90)).to be_within(0.0001).of(-Math::PI / 2)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns a float" do
|
|
19
|
+
expect(Vector2d.radians(0)).to be_an_instance_of(Float)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "is the inverse of .degrees" do
|
|
23
|
+
expect(Vector2d.degrees(Vector2d.radians(37.5))).to be_within(0.0001).of(37.5)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "raises an ArgumentError unless the angle is a real number" do
|
|
27
|
+
expect { Vector2d.radians(Complex(1, 2)) }
|
|
28
|
+
.to raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
[90, 90.0, Rational(90, 1), BigDecimal("90")].each do |angle|
|
|
32
|
+
context "with a #{angle.class} angle" do
|
|
33
|
+
subject(:radians) { Vector2d.radians(angle) }
|
|
34
|
+
|
|
35
|
+
it { is_expected.to be_an_instance_of(Float) }
|
|
36
|
+
|
|
37
|
+
it "converts the angle" do
|
|
38
|
+
expect(radians).to be_within(0.0001).of(Math::PI / 2)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe ".degrees" do
|
|
45
|
+
it "converts radians to degrees" do
|
|
46
|
+
expect(Vector2d.degrees(Math::PI / 2)).to be_within(0.0001).of(90.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "converts negative angles" do
|
|
50
|
+
expect(Vector2d.degrees(-Math::PI / 2)).to be_within(0.0001).of(-90.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "returns a float" do
|
|
54
|
+
expect(Vector2d.degrees(0)).to be_an_instance_of(Float)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "raises an ArgumentError unless the angle is a real number" do
|
|
58
|
+
expect { Vector2d.degrees(Complex(1, 2)) }
|
|
59
|
+
.to raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[1, 1.0, Rational(1, 1), BigDecimal("1")].each do |angle|
|
|
63
|
+
context "with a #{angle.class} angle" do
|
|
64
|
+
subject(:degrees) { Vector2d.degrees(angle) }
|
|
65
|
+
|
|
66
|
+
it { is_expected.to be_an_instance_of(Float) }
|
|
67
|
+
|
|
68
|
+
it "converts the angle" do
|
|
69
|
+
expect(degrees).to be_within(0.0001).of(57.2957)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe ".from_degrees" do
|
|
76
|
+
it "creates a vector from an angle" do
|
|
77
|
+
expect(Vector2d.from_degrees(90).y).to be_within(0.0001).of(1.0)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "defaults to unit length" do
|
|
81
|
+
expect(Vector2d.from_degrees(30).length).to be_within(0.0001).of(1.0)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "takes a length" do
|
|
85
|
+
expect(Vector2d.from_degrees(30, 2.0).length).to be_within(0.0001).of(2.0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "matches .from_angle with the angle converted" do
|
|
89
|
+
expect(Vector2d.from_degrees(30, 2.0))
|
|
90
|
+
.to eq(Vector2d.from_angle(Vector2d.radians(30), 2.0))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "raises an ArgumentError unless the angle is a real number" do
|
|
94
|
+
expect { Vector2d.from_degrees(Complex(1, 2)) }
|
|
95
|
+
.to raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "raises an ArgumentError unless the length is a real number" do
|
|
99
|
+
expect { Vector2d.from_degrees(30, Complex(1, 2)) }
|
|
100
|
+
.to raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
[45, 45.0, Rational(45, 1), BigDecimal("45")].each do |angle|
|
|
104
|
+
context "with a #{angle.class} angle" do
|
|
105
|
+
subject(:vector) { Vector2d.from_degrees(angle) }
|
|
106
|
+
|
|
107
|
+
it "returns float coordinates" do
|
|
108
|
+
expect(vector.to_a).to all(be_an_instance_of(Float))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "converts the angle" do
|
|
112
|
+
expect(vector.angle_in_degrees).to be_within(0.0001).of(45.0)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
[2, 2.0, Rational(2, 1), BigDecimal("2")].each do |length|
|
|
118
|
+
context "with a #{length.class} length" do
|
|
119
|
+
subject(:vector) { Vector2d.from_degrees(45, length) }
|
|
120
|
+
|
|
121
|
+
it "returns float coordinates" do
|
|
122
|
+
expect(vector.to_a).to all(be_an_instance_of(Float))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "applies the length" do
|
|
126
|
+
expect(vector.length).to be_within(0.0001).of(2.0)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe ".angle_to" do
|
|
133
|
+
let(:v1) { Vector2d.new(2, 3) }
|
|
134
|
+
let(:v2) { Vector2d.new(4, 5) }
|
|
135
|
+
|
|
136
|
+
it "calculates the angle from one vector to another" do
|
|
137
|
+
expect(Vector2d.angle_to(v1, v2)).to be_within(0.0001).of(-0.0867)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "signs the angle by direction of rotation" do
|
|
141
|
+
expect(Vector2d.angle_to(v2, v1)).to be_within(0.0001).of(0.0867)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "ignores the magnitude of the vectors" do
|
|
145
|
+
expect(
|
|
146
|
+
Vector2d.angle_to(v1 * 1000, v2 * 0.001)
|
|
147
|
+
).to be_within(0.0001).of(-0.0867)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "returns zero for identical vectors" do
|
|
151
|
+
expect(Vector2d.angle_to(v1, v1)).to eq(0.0)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "returns zero for parallel vectors" do
|
|
155
|
+
expect(Vector2d.angle_to(v1, v1 * 2.3)).to eq(0.0)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "returns PI for antiparallel vectors" do
|
|
159
|
+
expect(Vector2d.angle_to(v1, v1 * -2.3)).to eq(Math::PI)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "returns zero for a zero length vector" do
|
|
163
|
+
expect(Vector2d.angle_to(v1, Vector2d.new(0, 0))).to eq(0.0)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "stays within -PI..PI" do
|
|
167
|
+
angles = 360.times.map do |i|
|
|
168
|
+
Vector2d.angle_to(v1, v1.rotate(i * Math::PI / 180))
|
|
169
|
+
end
|
|
170
|
+
expect(angles).to all(be_between(-Math::PI, Math::PI))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "handles parallel vectors of any magnitude" do
|
|
174
|
+
angles = 1000.times.map do |i|
|
|
175
|
+
v = Vector2d.new((i + 1) * 0.37, (i + 1) * -1.13)
|
|
176
|
+
Vector2d.angle_to(v, v * ((i % 7) + 1))
|
|
177
|
+
end
|
|
178
|
+
expect(angles).to all(be_within(1e-12).of(0.0))
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe ".angle_between" do
|
|
183
|
+
let(:v1) { Vector2d.new(2, 3) }
|
|
184
|
+
let(:v2) { Vector2d.new(4, 5) }
|
|
185
|
+
|
|
186
|
+
it "calculates the unsigned angle between two vectors" do
|
|
187
|
+
expect(Vector2d.angle_between(v1, v2)).to be_within(0.0001).of(0.0867)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "ignores the order of the arguments" do
|
|
191
|
+
expect(Vector2d.angle_between(v2, v1))
|
|
192
|
+
.to eq(Vector2d.angle_between(v1, v2))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "ignores the magnitude of the vectors" do
|
|
196
|
+
expect(
|
|
197
|
+
Vector2d.angle_between(v1 * 1000, v2 * 0.001)
|
|
198
|
+
).to be_within(0.0001).of(0.0867)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "returns zero for parallel vectors" do
|
|
202
|
+
expect(Vector2d.angle_between(v1, v1 * 2.3)).to eq(0.0)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "returns PI for antiparallel vectors" do
|
|
206
|
+
expect(Vector2d.angle_between(v1, v1 * -2.3)).to eq(Math::PI)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "returns zero for a zero length vector" do
|
|
210
|
+
expect(Vector2d.angle_between(v1, Vector2d.new(0, 0))).to eq(0.0)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "stays within 0..PI" do
|
|
214
|
+
angles = 360.times.map do |i|
|
|
215
|
+
Vector2d.angle_between(v1, v1.rotate(i * Math::PI / 180))
|
|
216
|
+
end
|
|
217
|
+
expect(angles).to all(be_between(0, Math::PI))
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
describe "#angle" do
|
|
222
|
+
it "returns the angle" do
|
|
223
|
+
expect(vector.angle).to be_within(0.0001).of(0.9827)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
describe "#angle_in_degrees" do
|
|
228
|
+
it "returns the angle in degrees" do
|
|
229
|
+
expect(vector.angle_in_degrees).to be_within(0.0001).of(56.3099)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "is signed" do
|
|
233
|
+
expect(Vector2d.new(0, -1).angle_in_degrees).to be_within(0.0001).of(-90.0)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "matches #angle converted" do
|
|
237
|
+
expect(vector.angle_in_degrees).to eq(Vector2d.degrees(vector.angle))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "is zero for the zero vector" do
|
|
241
|
+
expect(Vector2d.new(0, 0).angle_in_degrees).to eq(0.0)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
[[0, 1], [0.0, 1.0], [Rational(0, 1), Rational(1, 1)],
|
|
245
|
+
[BigDecimal("0"), BigDecimal("1")]].each do |(x, y)|
|
|
246
|
+
context "with #{y.class} coordinates" do
|
|
247
|
+
subject(:degrees) { Vector2d.new(x, y).angle_in_degrees }
|
|
248
|
+
|
|
249
|
+
it { is_expected.to be_an_instance_of(Float) }
|
|
250
|
+
|
|
251
|
+
it "returns the angle in degrees" do
|
|
252
|
+
expect(degrees).to be_within(0.0001).of(90.0)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe "#angle_to" do
|
|
259
|
+
let(:comp) { Vector2d.new(3, 4) }
|
|
260
|
+
|
|
261
|
+
it "calculates the angle to the other vector" do
|
|
262
|
+
expect(
|
|
263
|
+
vector.angle_to(comp)
|
|
264
|
+
).to eq(Vector2d.angle_to(vector, comp))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "coerces the argument" do
|
|
268
|
+
expect(vector.angle_to([3, 4])).to eq(vector.angle_to(comp))
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
describe "#angle_between" do
|
|
273
|
+
let(:comp) { Vector2d.new(3, 4) }
|
|
274
|
+
|
|
275
|
+
it "calculates the angle between vectors" do
|
|
276
|
+
expect(
|
|
277
|
+
vector.angle_between(comp)
|
|
278
|
+
).to eq(Vector2d.angle_between(vector, comp))
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "coerces the argument" do
|
|
282
|
+
expect(vector.angle_between([3, 4])).to eq(vector.angle_between(comp))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe "#angle_with" do
|
|
287
|
+
let(:comp) { Vector2d.new(3, 4) }
|
|
288
|
+
|
|
289
|
+
it "is an alias of #angle_between" do
|
|
290
|
+
expect(vector.angle_with(comp)).to eq(vector.angle_between(comp))
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
describe "#to_polar" do
|
|
295
|
+
it "returns the length first" do
|
|
296
|
+
expect(vector.to_polar.first).to be_within(0.0001).of(3.6055)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "returns the angle second" do
|
|
300
|
+
expect(vector.to_polar.last).to be_within(0.0001).of(0.9827)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "is the inverse of Vector2d.from_angle" do
|
|
304
|
+
length, angle = vector.to_polar
|
|
305
|
+
|
|
306
|
+
expect(Vector2d.from_angle(angle, length))
|
|
307
|
+
.to eq(Vector2d.new(2.0, 3.0))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
context "with the zero vector" do
|
|
311
|
+
let(:vector) { Vector2d.new(0, 0) }
|
|
312
|
+
|
|
313
|
+
it "returns zero length and angle" do
|
|
314
|
+
expect(vector.to_polar).to eq([0.0, 0.0])
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
describe "#rotate" do
|
|
320
|
+
subject { vector.rotate(rotation).round(3) }
|
|
321
|
+
|
|
322
|
+
let(:vector) { Vector2d.new(1, 0) }
|
|
323
|
+
|
|
324
|
+
context "when roating by PI" do
|
|
325
|
+
let(:rotation) { Math::PI }
|
|
326
|
+
|
|
327
|
+
it { is_expected.to eq(Vector2d.new(-1, 0)) }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
context "when roating by PI/2" do
|
|
331
|
+
let(:rotation) { Math::PI / 2 }
|
|
332
|
+
|
|
333
|
+
it { is_expected.to eq(Vector2d.new(0, 1)) }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
context "when roating by -PI/2" do
|
|
337
|
+
let(:rotation) { -Math::PI / 2 }
|
|
338
|
+
|
|
339
|
+
it { is_expected.to eq(Vector2d.new(0, -1)) }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
context "when roating by PI/4" do
|
|
343
|
+
let(:rotation) { Math::PI / 4 }
|
|
344
|
+
|
|
345
|
+
it { is_expected.to eq(Vector2d.new(0.707, 0.707)) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
context "with a complex angle" do
|
|
349
|
+
it "raises an error" do
|
|
350
|
+
expect { vector.rotate(Complex(1, 2)) }.to(
|
|
351
|
+
raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
describe "#rotate_degrees" do
|
|
358
|
+
it "rotates the vector counterclockwise" do
|
|
359
|
+
expect(vector.rotate_degrees(90)).to eq(Vector2d.new(-3.0, 2.0))
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "rotates clockwise for a negative angle" do
|
|
363
|
+
expect(vector.rotate_degrees(-90).x).to be_within(0.0001).of(3.0)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it "matches #rotate with the angle converted" do
|
|
367
|
+
expect(vector.rotate_degrees(30)).to eq(vector.rotate(Vector2d.radians(30)))
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "leaves the length alone" do
|
|
371
|
+
expect(vector.rotate_degrees(30).length).to be_within(0.0001).of(vector.length)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "raises an ArgumentError unless the angle is a real number" do
|
|
375
|
+
expect { vector.rotate_degrees(Complex(1, 2)) }
|
|
376
|
+
.to raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
[90, 90.0, Rational(90, 1), BigDecimal("90")].each do |angle|
|
|
380
|
+
context "with a #{angle.class} angle" do
|
|
381
|
+
subject(:rotated) { vector.rotate_degrees(angle) }
|
|
382
|
+
|
|
383
|
+
it "returns float coordinates" do
|
|
384
|
+
expect(rotated.to_a).to all(be_an_instance_of(Float))
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "rotates the vector" do
|
|
388
|
+
expect(rotated).to eq(Vector2d.new(-3.0, 2.0))
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
describe "#rotate_around" do
|
|
395
|
+
subject { vector.rotate_around(center, Math::PI / 2).round(3) }
|
|
396
|
+
|
|
397
|
+
let(:vector) { Vector2d.new(2, 1) }
|
|
398
|
+
let(:center) { Vector2d.new(1, 1) }
|
|
399
|
+
|
|
400
|
+
it { is_expected.to eq(Vector2d.new(1, 2)) }
|
|
401
|
+
|
|
402
|
+
context "when the center is the origin" do
|
|
403
|
+
let(:center) { Vector2d.new(0, 0) }
|
|
404
|
+
|
|
405
|
+
it { is_expected.to eq(vector.rotate(Math::PI / 2).round(3)) }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
context "when the center is the vector itself" do
|
|
409
|
+
let(:center) { vector }
|
|
410
|
+
|
|
411
|
+
it { is_expected.to eq(vector) }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
context "with a coercible center" do
|
|
415
|
+
let(:center) { [1, 1] }
|
|
416
|
+
|
|
417
|
+
it { is_expected.to eq(Vector2d.new(1, 2)) }
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
describe "#perpendicular" do
|
|
422
|
+
it "returns a perpendicular vector" do
|
|
423
|
+
expect(vector.perpendicular).to eq(Vector2d.new(-3, 2))
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
it "turns the same way as #rotate" do
|
|
427
|
+
expect(vector.perpendicular.round(3))
|
|
428
|
+
.to eq(vector.rotate(Math::PI / 2).round(3))
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
describe "#perpendicular_cw" do
|
|
433
|
+
it "returns a perpendicular vector" do
|
|
434
|
+
expect(vector.perpendicular_cw).to eq(Vector2d.new(3, -2))
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
it "turns the opposite way from #perpendicular" do
|
|
438
|
+
expect(vector.perpendicular_cw).to eq(vector.perpendicular.reverse)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -2,36 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
describe Vector2d::
|
|
5
|
+
describe Vector2d::Arithmetic do
|
|
6
6
|
subject(:vector) { Vector2d.new(2, 3) }
|
|
7
7
|
|
|
8
|
-
describe ".cross_product" do
|
|
9
|
-
let(:v1) { Vector2d.new(2, 3) }
|
|
10
|
-
let(:v2) { Vector2d.new(4, 5) }
|
|
11
|
-
|
|
12
|
-
it "calculates the cross product between two vectors" do
|
|
13
|
-
expect(Vector2d.cross_product(v1, v2)).to eq(-2.0)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
describe ".dot_product" do
|
|
18
|
-
let(:v1) { Vector2d.new(2, 3) }
|
|
19
|
-
let(:v2) { Vector2d.new(4, 5) }
|
|
20
|
-
|
|
21
|
-
it "calculates the dot product between two vectors" do
|
|
22
|
-
expect(Vector2d.dot_product(v1, v2)).to eq(23)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe ".angle_between" do
|
|
27
|
-
let(:v1) { Vector2d.new(2, 3) }
|
|
28
|
-
let(:v2) { Vector2d.new(4, 5) }
|
|
29
|
-
|
|
30
|
-
it "calculates the angle between two vectors" do
|
|
31
|
-
expect(Vector2d.angle_between(v1, v2)).to be_within(0.0001).of(0.0867)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
8
|
describe "*" do
|
|
36
9
|
context "with vector" do
|
|
37
10
|
subject(:vector) { Vector2d.new(2, 3) * Vector2d.new(3, 4) }
|
|
@@ -47,6 +20,28 @@ describe Vector2d::Calculations do
|
|
|
47
20
|
it "multiplies both members" do
|
|
48
21
|
expect(vector).to eq(Vector2d.new(6, 9))
|
|
49
22
|
end
|
|
23
|
+
|
|
24
|
+
its(:x) { is_expected.to be_a(Integer) }
|
|
25
|
+
its(:y) { is_expected.to be_a(Integer) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context "with a float" do
|
|
29
|
+
subject(:vector) { Vector2d.new(2, 3) * 3.0 }
|
|
30
|
+
|
|
31
|
+
it "multiplies both members" do
|
|
32
|
+
expect(vector).to eq(Vector2d.new(6.0, 9.0))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
its(:x) { is_expected.to be_a(Float) }
|
|
36
|
+
its(:y) { is_expected.to be_a(Float) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "with a complex number" do
|
|
40
|
+
it "raises a TypeError" do
|
|
41
|
+
expect { Vector2d.new(2, 3) * Complex(1, 2) }.to(
|
|
42
|
+
raise_error(TypeError, "Complex can't be coerced into Vector2d")
|
|
43
|
+
)
|
|
44
|
+
end
|
|
50
45
|
end
|
|
51
46
|
end
|
|
52
47
|
|
|
@@ -65,6 +60,9 @@ describe Vector2d::Calculations do
|
|
|
65
60
|
it "divides both members" do
|
|
66
61
|
expect(vector).to eq(Vector2d.new(2, 4))
|
|
67
62
|
end
|
|
63
|
+
|
|
64
|
+
its(:x) { is_expected.to be_a(Integer) }
|
|
65
|
+
its(:y) { is_expected.to be_a(Integer) }
|
|
68
66
|
end
|
|
69
67
|
end
|
|
70
68
|
|
|
@@ -83,6 +81,9 @@ describe Vector2d::Calculations do
|
|
|
83
81
|
it "adds to both members" do
|
|
84
82
|
expect(vector).to eq(Vector2d.new(4, 5))
|
|
85
83
|
end
|
|
84
|
+
|
|
85
|
+
its(:x) { is_expected.to be_a(Integer) }
|
|
86
|
+
its(:y) { is_expected.to be_a(Integer) }
|
|
86
87
|
end
|
|
87
88
|
end
|
|
88
89
|
|
|
@@ -101,50 +102,39 @@ describe Vector2d::Calculations do
|
|
|
101
102
|
it "subtracts from both members" do
|
|
102
103
|
expect(vector).to eq(Vector2d.new(0, 1))
|
|
103
104
|
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
it "calulates the cross product" do
|
|
111
|
-
expect(
|
|
112
|
-
vector.cross_product(comp)
|
|
113
|
-
).to eq(Vector2d.cross_product(vector, comp))
|
|
106
|
+
its(:x) { is_expected.to be_a(Integer) }
|
|
107
|
+
its(:y) { is_expected.to be_a(Integer) }
|
|
114
108
|
end
|
|
115
109
|
end
|
|
116
110
|
|
|
117
|
-
describe "
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
it "returns the distance between two vectors" do
|
|
121
|
-
expect(vector.distance(comp)).to be_within(0.0001).of(1.4142)
|
|
111
|
+
describe "-@" do
|
|
112
|
+
it "reverses the vector" do
|
|
113
|
+
expect(-Vector2d.new(2, 3)).to eq(Vector2d.new(-2, -3))
|
|
122
114
|
end
|
|
123
|
-
end
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
it "is the same as #reverse" do
|
|
117
|
+
expect(-vector).to eq(vector.reverse)
|
|
118
|
+
end
|
|
127
119
|
|
|
128
|
-
it "returns the
|
|
129
|
-
expect(vector
|
|
120
|
+
it "returns the original vector when applied twice" do
|
|
121
|
+
expect(-(-vector)).to eq(vector)
|
|
130
122
|
end
|
|
131
123
|
end
|
|
132
124
|
|
|
133
|
-
describe "
|
|
134
|
-
|
|
125
|
+
describe "+@" do
|
|
126
|
+
it "returns the vector unchanged" do
|
|
127
|
+
expect(+Vector2d.new(2, 3)).to eq(Vector2d.new(2, 3))
|
|
128
|
+
end
|
|
135
129
|
|
|
136
|
-
it "
|
|
137
|
-
expect(vector
|
|
130
|
+
it "returns the same object" do
|
|
131
|
+
expect(+vector).to be(vector)
|
|
138
132
|
end
|
|
139
133
|
end
|
|
140
134
|
|
|
141
|
-
describe "#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
it "calculates the angle between vectors" do
|
|
145
|
-
expect(
|
|
146
|
-
vector.angle_between(comp)
|
|
147
|
-
).to eq(Vector2d.angle_between(vector, comp))
|
|
135
|
+
describe "#reverse" do
|
|
136
|
+
it "reverses the vector" do
|
|
137
|
+
expect(vector.reverse).to eq(Vector2d.new(-2, -3))
|
|
148
138
|
end
|
|
149
139
|
end
|
|
150
140
|
end
|