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
data/README.md
CHANGED
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
# Vector2d
|
|
5
5
|
|
|
6
|
-
Vector2d
|
|
7
|
-
|
|
6
|
+
Vector2d is a library for handling two-dimensional vectors and coordinates.
|
|
7
|
+
It's fully featured, but has a particular focus on image processing, including constrained scaling and geometry string parsing.
|
|
8
|
+
|
|
9
|
+
It is strictly immutable and safe for use with Ractors.
|
|
10
|
+
It has no runtime dependencies, and is about 1.6x faster than the stdlib `Vector` class.
|
|
11
|
+
Every method is documented with examples in the [API documentation](https://rubydoc.info/gems/vector2d).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Vector2d requires Ruby 3.4 or later. Add the gem to your Gemfile and run `bundle install`.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "vector2d"
|
|
19
|
+
```
|
|
8
20
|
|
|
9
21
|
## Quick example
|
|
10
22
|
|
|
@@ -13,29 +25,330 @@ require 'vector2d'
|
|
|
13
25
|
|
|
14
26
|
vector = Vector2d(50, 70)
|
|
15
27
|
|
|
16
|
-
vector.aspect_ratio # => 0.
|
|
17
|
-
vector.length # => 86.
|
|
28
|
+
vector.aspect_ratio # => 0.7142857142857143
|
|
29
|
+
vector.length # => 86.02325267042627
|
|
18
30
|
|
|
19
31
|
vector * 2 # => Vector2d(100,140)
|
|
20
32
|
vector + Vector2d(20, 30) # => Vector2d(70,100)
|
|
21
33
|
|
|
22
|
-
vector.fit(Vector2d(64, 64)) # => Vector2d(64
|
|
34
|
+
vector.fit(Vector2d(64, 64)) # => Vector2d(45.714285714285715,64.0)
|
|
23
35
|
|
|
24
36
|
Vector2d.parse([50, 70]) # => Vector2d(50,70)
|
|
25
37
|
Vector2d.parse("50x70") # => Vector2d(50,70)
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
##
|
|
40
|
+
## Fitting
|
|
41
|
+
|
|
42
|
+
These are the three operations from web image sizing:
|
|
43
|
+
|
|
44
|
+
| Vector2d | Scales to | CSS `object-fit` | ImageMagick geometry |
|
|
45
|
+
| ---------------------------- | -------------------------------------------- | ---------------- | -------------------- |
|
|
46
|
+
| `fit(other)` | the largest size that fits inside the box | `contain` | `WxH` |
|
|
47
|
+
| `cover(other)` | the smallest size that fills the box | `cover` | `WxH^` |
|
|
48
|
+
| `fit(other, upscale: false)` | the largest size that fits, never scaling up | `scale-down` | `WxH>` |
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
image = Vector2d(1600, 1200)
|
|
52
|
+
small = Vector2d(120, 90)
|
|
53
|
+
thumbnail = Vector2d(200, 200)
|
|
54
|
+
|
|
55
|
+
image.fit(thumbnail) # => Vector2d(200.0,150.0)
|
|
56
|
+
image.cover(thumbnail) # => Vector2d(266.66666666666663,200.0)
|
|
57
|
+
|
|
58
|
+
small.fit(thumbnail) # => Vector2d(200.0,150.0)
|
|
59
|
+
small.fit(thumbnail, upscale: false) # => Vector2d(120,90)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The results are floats, use `#round` and `#to_s` to get a string representation to use with other tools.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
image.fit(thumbnail).round.to_s # => "200x150"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To constrain only one axis, leave the other blank or zero:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
image.fit(Vector2d("800x")) # => Vector2d(800.0,600.0)
|
|
72
|
+
image.fit(Vector2d(800, 0)) # => Vector2d(800.0,600.0)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`#fits?` and `#covers?` ask the question without doing the scaling.
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
image.fits?(thumbnail) # => false
|
|
79
|
+
small.fits?(thumbnail) # => true
|
|
80
|
+
image.covers?(thumbnail) # => true
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A vector doubles as a rectangle, and a few properties describe one.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
image.area # => 1920000
|
|
87
|
+
image.aspect_ratio # => 1.3333333333333333
|
|
88
|
+
image.landscape? # => true
|
|
89
|
+
image.portrait? # => false
|
|
90
|
+
image.square? # => false
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Parsing and coercion
|
|
94
|
+
|
|
95
|
+
`Vector2d.parse` is quite liberal: it accepts numbers, arrays, hashes, strings, other vectors, and the `Vector` and `Matrix` classes from the standard library.
|
|
96
|
+
Strings are written as `"50x70"` or `"50,70"`. The separator is case insensitive and whitespace is ignored.
|
|
97
|
+
Coordinates can be signed, and they retain their numeric type. It is also aliased as `Vector2d()` shorthand.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Vector2d.parse("50x70") # => Vector2d(50,70)
|
|
101
|
+
Vector2d.parse("50.0x70") # => Vector2d(50.0,70)
|
|
102
|
+
Vector2d.parse("-50X70") # => Vector2d(-50,70)
|
|
103
|
+
Vector2d.parse("50, 70") # => Vector2d(50,70)
|
|
104
|
+
|
|
105
|
+
Vector2d.parse([50, 70]) # => Vector2d(50,70)
|
|
106
|
+
Vector2d.parse({ x: 50, y: 70 }) # => Vector2d(50,70)
|
|
107
|
+
Vector2d.parse(50) # => Vector2d(50,50)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Coordinates are real numbers: integers, floats, rationals and decimals.
|
|
111
|
+
|
|
112
|
+
This isn't limited to construction. Every method that takes a vector runs its argument through `.parse`, so all of the forms above work at any call site.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
Vector2d(2, 3).distance("5x7") # => 5.0
|
|
116
|
+
Vector2d(2, 3).min([1, 5]) # => Vector2d(1,3)
|
|
117
|
+
Vector2d(23, 47).snap([10, 5]) # => Vector2d(20,45)
|
|
118
|
+
Vector2d(3, 4).approx_equal?([3.0, 4.0]) # => true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`#==` is the exception. It compares coordinates exactly and doesn't coerce, so that vectors stay usable as hash keys.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Vector2d(3, 4) == [3, 4] # => false
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Vector2d.new` takes exactly two coordinates and nothing else.
|
|
128
|
+
|
|
129
|
+
## Arithmetic
|
|
130
|
+
|
|
131
|
+
Arithmetic operations have the same type semantics as Ruby numbers, so integer division truncates:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
vector = Vector2d(50, 70)
|
|
135
|
+
|
|
136
|
+
vector / 20 # => Vector2d(2,3)
|
|
137
|
+
vector / 20.0 # => Vector2d(2.5,3.5)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A scalar argument applies to both axes, and vectors coerce, so it can come first:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
vector - 10 # => Vector2d(40,60)
|
|
144
|
+
vector * 2 # => Vector2d(100,140)
|
|
145
|
+
2 * vector # => Vector2d(100,140)
|
|
146
|
+
vector.max(60) # => Vector2d(60,70)
|
|
147
|
+
vector.clamp(0, 60) # => Vector2d(50,60)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A vector argument applies one axis at a time:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
vector + Vector2d(20, 30) # => Vector2d(70,100)
|
|
154
|
+
vector.min(Vector2d(60, 60)) # => Vector2d(50,60)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Unary `-` points a vector the other way, and `#reverse` is the same thing spelled out:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
-vector # => Vector2d(-50,-70)
|
|
161
|
+
vector.reverse # => Vector2d(-50,-70)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`#with_x` and `#with_y` replace a single axis:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
vector.with_x(100) # => Vector2d(100,70)
|
|
168
|
+
vector.with_y(100) # => Vector2d(50,100)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`#round`, `#ceil` and `#floor` work one axis at a time, the way their counterparts on Ruby's numerics do, and all three take a number of digits.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
Vector2d(2.44, 3.66).round # => Vector2d(2,4)
|
|
175
|
+
Vector2d(2.44, 3.66).ceil # => Vector2d(3,4)
|
|
176
|
+
Vector2d(2.44, 3.66).floor # => Vector2d(2,3)
|
|
177
|
+
Vector2d(2.44, 3.66).round(1) # => Vector2d(2.4,3.7)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`#snap` rounds each axis to the nearest multiple of a step.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
Vector2d(23, 47).snap(10) # => Vector2d(20,50)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`#abs` drops the signs and `#sign` reduces each axis to -1, 0 or 1.
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
Vector2d(-2.5, 3.5).abs # => Vector2d(2.5,3.5)
|
|
190
|
+
Vector2d(-2.5, 3.5).sign # => Vector2d(-1,1)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Length and distance
|
|
194
|
+
|
|
195
|
+
`#resize` scales a vector to a given length and `#normalize` scales it to one.
|
|
196
|
+
`#limit_length` caps the length at a maximum and `#clamp_length` bounds it at both ends, scaling a short vector up to the minimum.
|
|
197
|
+
All of them keep the direction, and `#clamp_length` takes its bounds the way `#clamp` does, as two arguments or as a range.
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
vector = Vector2d(3, 4)
|
|
201
|
+
|
|
202
|
+
vector.length # => 5.0
|
|
203
|
+
vector.length_squared # => 25
|
|
204
|
+
vector.normalize # => Vector2d(0.6000000000000001,0.8)
|
|
205
|
+
vector.resize(10) # => Vector2d(6.0,8.0)
|
|
206
|
+
|
|
207
|
+
vector.limit_length(3) # => Vector2d(1.7999999999999998,2.4)
|
|
208
|
+
vector.limit_length(10) # => Vector2d(3.0,4.0)
|
|
209
|
+
vector.clamp_length(10, 20) # => Vector2d(6.0,8.0)
|
|
210
|
+
vector.clamp_length(1..3) # => Vector2d(1.7999999999999998,2.4)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`#normalized?` asks whether a vector is already of length one.
|
|
214
|
+
Distances come in the usual flavours, and `#direction_to` is the unit vector pointing from one vector to another.
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
origin = Vector2d(2, 3)
|
|
218
|
+
|
|
219
|
+
origin.distance("5x7") # => 5.0
|
|
220
|
+
origin.distance_squared([5, 7]) # => 25
|
|
221
|
+
origin.manhattan_distance([5, 7]) # => 7
|
|
222
|
+
origin.chebyshev_distance([5, 7]) # => 4
|
|
223
|
+
origin.direction_to([5, 7]) # => Vector2d(0.6000000000000001,0.8)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Angles and rotation
|
|
227
|
+
|
|
228
|
+
Angles are radians, measured from the positive x axis.
|
|
229
|
+
Positive angles turn counterclockwise, and so do `#rotate` and `#perpendicular`.
|
|
230
|
+
The y axis grows upwards, putting `Vector2d.up` at `(0, 1)`.
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
Vector2d.right # => Vector2d(1,0)
|
|
234
|
+
Vector2d.up # => Vector2d(0,1)
|
|
235
|
+
Vector2d.left # => Vector2d(-1,0)
|
|
236
|
+
Vector2d.down # => Vector2d(0,-1)
|
|
237
|
+
|
|
238
|
+
Vector2d.up.angle # => 1.5707963267948966
|
|
239
|
+
Vector2d(2, 3).perpendicular # => Vector2d(-3,2)
|
|
240
|
+
Vector2d.up.angle_to(Vector2d.right) # => -1.5707963267948966
|
|
241
|
+
Vector2d.up.angle_between(Vector2d.right) # => 1.5707963267948966
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`#rotate` turns a vector about the origin and `#rotate_around` about another point.
|
|
245
|
+
`#perpendicular` is a quarter turn counterclockwise, `#perpendicular_cw` the other way.
|
|
246
|
+
`Vector2d.from_angle` builds a vector from an angle and a length, and `#to_polar` takes one apart again.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
Vector2d(2, 3).perpendicular_cw # => Vector2d(3,-2)
|
|
250
|
+
Vector2d(2, 1).rotate_around([1, 1], Math::PI / 2) # => Vector2d(1.0,2.0)
|
|
251
|
+
|
|
252
|
+
Vector2d.from_angle(0, 5) # => Vector2d(5.0,0.0)
|
|
253
|
+
Vector2d(3, 4).to_polar # => [5.0, 0.9272952180016122]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Where degrees are easier to read, there are convenience methods for them.
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
Vector2d.from_degrees(45) # => Vector2d(0.7071067811865476,0.7071067811865475)
|
|
260
|
+
Vector2d.up.angle_in_degrees # => 90.0
|
|
261
|
+
Vector2d(2, 3).rotate_degrees(90) # => Vector2d(-3.0,2.0)
|
|
262
|
+
|
|
263
|
+
Vector2d.radians(90) # => 1.5707963267948966
|
|
264
|
+
Vector2d.degrees(Math::PI) # => 180.0
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Comparison
|
|
268
|
+
|
|
269
|
+
`#==` compares coordinates exactly, so use `#approx_equal?` on anything you've done arithmetic to.
|
|
270
|
+
Its default tolerance, the one `#parallel?` and `#perpendicular?` use, is a few ulps scaled by the magnitude of the vectors; a tolerance of your own is an absolute distance, and isn't scaled.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
drifted = Vector2d(0.1, 0.2) * 3
|
|
274
|
+
|
|
275
|
+
drifted # => Vector2d(0.30000000000000004,0.6000000000000001)
|
|
276
|
+
drifted == Vector2d(0.3, 0.6) # => false
|
|
277
|
+
drifted.approx_equal?([0.3, 0.6]) # => true
|
|
278
|
+
|
|
279
|
+
Vector2d(1e-20, 0).zero? # => false
|
|
280
|
+
Vector2d(1e-20, 0).approx_zero? # => true
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`#eql?` is stricter still and tells coordinate types apart, and `#hash` follows it, so the two vectors below are different hash keys.
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
Vector2d(3, 4) == Vector2d(3.0, 4.0) # => true
|
|
287
|
+
Vector2d(3, 4).eql?(Vector2d(3.0, 4.0)) # => false
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The directional predicates compare directions and ignore magnitudes, and `#finite?` and `#nan?` ask about the coordinates.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
Vector2d(2, 3).parallel?([-4, -6]) # => true
|
|
294
|
+
Vector2d(2, 3).opposite?([-4, -6]) # => true
|
|
295
|
+
Vector2d(2, 3).perpendicular?([-3, 2]) # => true
|
|
296
|
+
Vector2d(2, 3).independent?([3, 2]) # => true
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Pattern matching
|
|
300
|
+
|
|
301
|
+
Vectors deconstruct to an array or a hash, so they can be matched against either kind of pattern.
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
case Vector2d(3, 4)
|
|
305
|
+
in [0, 0] then :origin
|
|
306
|
+
in [Integer => x, Integer => y] then x + y
|
|
307
|
+
end # => 7
|
|
308
|
+
|
|
309
|
+
case Vector2d(0, 4)
|
|
310
|
+
in {x: 0} then :on_y_axis
|
|
311
|
+
in {y: 0} then :on_x_axis
|
|
312
|
+
end # => :on_y_axis
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Conversions
|
|
316
|
+
|
|
317
|
+
A vector converts to the plain Ruby representations, `#to_s` writes the geometry string back out, and `#to_i_vector` and `#to_f_vector` convert the coordinates while keeping the class.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
Vector2d(2.5, 3.5).to_a # => [2.5, 3.5]
|
|
321
|
+
Vector2d(2.5, 3.5).to_hash # => {x: 2.5, y: 3.5}
|
|
322
|
+
Vector2d(2.5, 3.5).to_s # => "2.5x3.5"
|
|
323
|
+
Vector2d(2.5, 3.5).to_i_vector # => Vector2d(2,3)
|
|
324
|
+
Vector2d(2, 3).to_f_vector # => Vector2d(2.0,3.0)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Standard library compatibility
|
|
328
|
+
|
|
329
|
+
Vectors convert to and from the `Matrix` and `Vector` classes in the standard library, and the operators accept both.
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
Vector2d(2, 3).to_vector # => Vector[2, 3]
|
|
333
|
+
Vector2d(2, 3).to_matrix # => Matrix[[2], [3]]
|
|
334
|
+
Vector2d.parse(Vector[2, 3]) # => Vector2d(2,3)
|
|
335
|
+
Vector2d.parse(Matrix[[2], [3]]) # => Vector2d(2,3)
|
|
336
|
+
|
|
337
|
+
Vector[1, 2] + Vector2d(3, 4) # => Vector2d(4,6)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`#transform` multiplies a 2x2 matrix by the vector.
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
Vector2d(3, 4).transform(Matrix[[0, -1], [1, 0]]) # => Vector2d(-4,3)
|
|
344
|
+
```
|
|
29
345
|
|
|
30
|
-
|
|
346
|
+
The `matrix` library is a bundled gem, so applications using these methods need `gem "matrix"` in their Gemfile. It's only loaded when a conversion needs it.
|
|
31
347
|
|
|
32
348
|
## Contributing
|
|
33
349
|
|
|
34
|
-
Bug reports and pull requests are welcome on
|
|
35
|
-
[
|
|
36
|
-
[CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests and how
|
|
37
|
-
commits are formatted, and note that this project ships with a
|
|
38
|
-
[code of conduct](CODE_OF_CONDUCT.md).
|
|
350
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/elektronaut/vector2d).
|
|
351
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests and how commits are formatted, and note that this project ships with a [code of conduct](CODE_OF_CONDUCT.md).
|
|
39
352
|
|
|
40
353
|
## License
|
|
41
354
|
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rspec/core/rake_task"
|
|
5
|
+
require "yard"
|
|
5
6
|
|
|
6
7
|
# release-please creates the tag and the release commit.
|
|
7
8
|
Rake::Task["release:source_control_push"].clear
|
|
@@ -12,3 +13,20 @@ task default: :spec
|
|
|
12
13
|
|
|
13
14
|
desc "Run tests"
|
|
14
15
|
task test: :spec
|
|
16
|
+
|
|
17
|
+
desc "Benchmark against the standard library Vector"
|
|
18
|
+
task :benchmark do
|
|
19
|
+
ruby "benchmark/vector_comparison.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Generate API documentation"
|
|
23
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
24
|
+
t.stats_options = ["--list-undoc"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
namespace :doc do
|
|
28
|
+
desc "Run the doc examples and check the type tags"
|
|
29
|
+
RSpec::Core::RakeTask.new(:check) do |t|
|
|
30
|
+
t.pattern = "spec/lib/vector2d_{documentation,tags}_spec.rb"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
|
|
5
|
+
require "matrix"
|
|
6
|
+
require "vector2d"
|
|
7
|
+
|
|
8
|
+
# Times Vector2d against the standard library Vector, one operation at
|
|
9
|
+
# a time, and prints a table of the results.
|
|
10
|
+
#
|
|
11
|
+
# bundle exec rake benchmark
|
|
12
|
+
# ITERATIONS=5000000 REPETITIONS=3 bundle exec ruby \
|
|
13
|
+
# benchmark/vector_comparison.rb
|
|
14
|
+
#
|
|
15
|
+
module VectorComparison
|
|
16
|
+
ITERATIONS = Integer(ENV.fetch("ITERATIONS", "1000000"))
|
|
17
|
+
REPETITIONS = Integer(ENV.fetch("REPETITIONS", "5"))
|
|
18
|
+
|
|
19
|
+
ANGLE = Math::PI / 4
|
|
20
|
+
SCALAR = 2.0
|
|
21
|
+
|
|
22
|
+
V1 = Vector2d(3.0, 4.0)
|
|
23
|
+
V2 = Vector2d(1.5, 2.5)
|
|
24
|
+
S1 = Vector[3.0, 4.0]
|
|
25
|
+
S2 = Vector[1.5, 2.5]
|
|
26
|
+
|
|
27
|
+
# One operation, written both ways. The note names the shape of the
|
|
28
|
+
# standard library version where Vector has no equivalent method.
|
|
29
|
+
Case = Struct.new(:name, :ours, :theirs, :note)
|
|
30
|
+
|
|
31
|
+
# A timed case. Times are seconds for ITERATIONS operations.
|
|
32
|
+
Result = Struct.new(:name, :ours, :theirs, :note) do
|
|
33
|
+
def ratio = theirs / ours
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
CASES = [
|
|
37
|
+
Case.new("new", -> { Vector2d.new(3.0, 4.0) }, -> { Vector[3.0, 4.0] }),
|
|
38
|
+
Case.new("+", -> { V1 + V2 }, -> { S1 + S2 }),
|
|
39
|
+
Case.new("-", -> { V1 - V2 }, -> { S1 - S2 }),
|
|
40
|
+
Case.new("* scalar", -> { V1 * SCALAR }, -> { S1 * SCALAR }),
|
|
41
|
+
Case.new("* vector", -> { V1 * V2 },
|
|
42
|
+
-> { S1.map2(S2) { |a, b| a * b } },
|
|
43
|
+
"Vector#map2"),
|
|
44
|
+
Case.new("dot", -> { V1.dot(V2) }, -> { S1.inner_product(S2) }),
|
|
45
|
+
Case.new("length", -> { V1.length }, -> { S1.magnitude }),
|
|
46
|
+
Case.new("normalize", -> { V1.normalize }, -> { S1.normalize }),
|
|
47
|
+
Case.new("rotate", -> { V1.rotate(ANGLE) },
|
|
48
|
+
lambda {
|
|
49
|
+
cos = Math.cos(ANGLE)
|
|
50
|
+
sin = Math.sin(ANGLE)
|
|
51
|
+
Vector[(S1[0] * cos) - (S1[1] * sin),
|
|
52
|
+
(S1[0] * sin) + (S1[1] * cos)]
|
|
53
|
+
},
|
|
54
|
+
"the rotation written out"),
|
|
55
|
+
Case.new("distance", -> { V1.distance(V2) }, -> { (S1 - S2).magnitude })
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
ROW = "%-12<name>s %11<ours>s %11<theirs>s %8<ratio>s"
|
|
59
|
+
RULE = "-" * 45
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
def run
|
|
64
|
+
results = CASES.map { |benchmark_case| measure_case(benchmark_case) }
|
|
65
|
+
puts(banner + table(results) + notes(results))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def measure_case(benchmark_case)
|
|
69
|
+
Result.new(benchmark_case.name,
|
|
70
|
+
best_time(benchmark_case.ours),
|
|
71
|
+
best_time(benchmark_case.theirs),
|
|
72
|
+
benchmark_case.note)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def best_time(callable)
|
|
76
|
+
(ITERATIONS / 10).times { callable.call }
|
|
77
|
+
Array.new(REPETITIONS) { measure(callable) }.min
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def measure(callable)
|
|
81
|
+
GC.start
|
|
82
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
83
|
+
ITERATIONS.times { callable.call }
|
|
84
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def banner
|
|
88
|
+
["Vector2d #{Vector2d::VERSION} against the standard library Vector",
|
|
89
|
+
"Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM}), " \
|
|
90
|
+
"#{delimited(ITERATIONS)} operations, best of #{REPETITIONS}",
|
|
91
|
+
""]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def table(results)
|
|
95
|
+
[format(ROW, name: "Operation", ours: "Vector2d", theirs: "Vector",
|
|
96
|
+
ratio: "Ratio"),
|
|
97
|
+
RULE,
|
|
98
|
+
*results.map { |result| result_row(result) },
|
|
99
|
+
RULE,
|
|
100
|
+
format(ROW, name: "geomean", ours: "", theirs: "",
|
|
101
|
+
ratio: format("%.2fx", geomean(results)))]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def result_row(result)
|
|
105
|
+
format(ROW,
|
|
106
|
+
name: result.note ? "#{result.name} *" : result.name,
|
|
107
|
+
ours: format("%.4f s", result.ours),
|
|
108
|
+
theirs: format("%.4f s", result.theirs),
|
|
109
|
+
ratio: format("%.2fx", result.ratio))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def notes(results)
|
|
113
|
+
["",
|
|
114
|
+
"Ratio is Vector time over Vector2d time. Above 1.00x is faster.",
|
|
115
|
+
*results.select(&:note).map do |result|
|
|
116
|
+
"* #{result.name}: no equivalent in Vector, timed as #{result.note}."
|
|
117
|
+
end]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def geomean(results)
|
|
121
|
+
Math.exp(results.sum { |result| Math.log(result.ratio) } / results.length)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def delimited(number)
|
|
125
|
+
number.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
VectorComparison.run if $PROGRAM_NAME == __FILE__
|