vector2d 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +1 -1
  3. data/.gitignore +3 -1
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +1 -1
  6. data/.yardopts +11 -0
  7. data/CHANGELOG.md +216 -0
  8. data/Gemfile +5 -1
  9. data/Gemfile.lock +35 -5
  10. data/README.md +325 -12
  11. data/Rakefile +18 -0
  12. data/benchmark/vector_comparison.rb +129 -0
  13. data/lib/vector2d/angles.rb +315 -0
  14. data/lib/vector2d/arithmetic.rb +97 -0
  15. data/lib/vector2d/comparison.rb +188 -0
  16. data/lib/vector2d/componentwise.rb +239 -0
  17. data/lib/vector2d/constructors.rb +137 -0
  18. data/lib/vector2d/conversions.rb +139 -0
  19. data/lib/vector2d/coordinates.rb +22 -0
  20. data/lib/vector2d/deprecation.rb +16 -0
  21. data/lib/vector2d/dimensions.rb +297 -0
  22. data/lib/vector2d/interpolation.rb +191 -0
  23. data/lib/vector2d/lengths.rb +284 -0
  24. data/lib/vector2d/matrix_interop.rb +93 -0
  25. data/lib/vector2d/parsing.rb +142 -0
  26. data/lib/vector2d/projection.rb +208 -0
  27. data/lib/vector2d/version.rb +2 -1
  28. data/lib/vector2d.rb +172 -60
  29. data/spec/lib/vector2d/angles_spec.rb +441 -0
  30. data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
  31. data/spec/lib/vector2d/comparison_spec.rb +358 -0
  32. data/spec/lib/vector2d/componentwise_spec.rb +300 -0
  33. data/spec/lib/vector2d/constructors_spec.rb +299 -0
  34. data/spec/lib/vector2d/conversions_spec.rb +234 -0
  35. data/spec/lib/vector2d/dimensions_spec.rb +820 -0
  36. data/spec/lib/vector2d/interpolation_spec.rb +322 -0
  37. data/spec/lib/vector2d/lengths_spec.rb +457 -0
  38. data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
  39. data/spec/lib/vector2d/parsing_spec.rb +329 -0
  40. data/spec/lib/vector2d/projection_spec.rb +306 -0
  41. data/spec/lib/vector2d_documentation_spec.rb +38 -0
  42. data/spec/lib/vector2d_immutability_spec.rb +263 -0
  43. data/spec/lib/vector2d_spec.rb +90 -50
  44. data/spec/lib/vector2d_subclassing_spec.rb +210 -0
  45. data/spec/lib/vector2d_tags_spec.rb +43 -0
  46. data/spec/spec_helper.rb +2 -0
  47. data/spec/support/doc_examples/comments.rb +65 -0
  48. data/spec/support/doc_examples/markdown.rb +50 -0
  49. data/spec/support/doc_examples.rb +94 -0
  50. data/spec/support/doc_tags.rb +151 -0
  51. data/spec/support/shared_examples/class_preserving_method.rb +10 -0
  52. data/spec/support/shared_examples/deprecated_method.rb +25 -0
  53. data/spec/support/shared_examples/parsed_vector.rb +11 -0
  54. data/vector2d.gemspec +2 -1
  55. metadata +42 -13
  56. data/.travis.yml +0 -10
  57. data/lib/vector2d/calculations.rb +0 -141
  58. data/lib/vector2d/coercions.rb +0 -64
  59. data/lib/vector2d/fitting.rb +0 -60
  60. data/lib/vector2d/properties.rb +0 -46
  61. data/lib/vector2d/transformations.rb +0 -98
  62. data/spec/lib/vector2d/coercions_spec.rb +0 -59
  63. data/spec/lib/vector2d/fitting_spec.rb +0 -70
  64. data/spec/lib/vector2d/properties_spec.rb +0 -47
  65. data/spec/lib/vector2d/transformations_spec.rb +0 -139
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Vector2d
4
- module Coercions
5
- def coerce(other)
6
- case other
7
- when Vector2d
8
- [other, self]
9
- when Array, Numeric, String, Hash
10
- [Vector2d.parse(other), self]
11
- else
12
- raise TypeError, "#{self.class} can't be coerced into #{other.class}"
13
- end
14
- end
15
-
16
- # Renders vector as a pretty string.
17
- #
18
- # Vector2d(2, 3).inspect # => "Vector2d(2,3)"
19
- #
20
- def inspect
21
- "Vector2d(#{x},#{y})"
22
- end
23
-
24
- # Converts vector to array.
25
- #
26
- # Vector2d(2, 3).to_a # => [2,3]
27
- #
28
- def to_a
29
- [x, y]
30
- end
31
-
32
- # Converts vector to hash.
33
- #
34
- # Vector2d(2, 3).to_hash # => {x: 2, y: 3}
35
- #
36
- def to_hash
37
- { x: x, y: y }
38
- end
39
-
40
- # Converts vector to fixnums.
41
- #
42
- # Vector2d(2.0, 3.0).to_i_vector # => Vector2d(2,3)
43
- #
44
- def to_i_vector
45
- self.class.new(x.to_i, y.to_i)
46
- end
47
-
48
- # Converts vector to floats.
49
- #
50
- # Vector2d(2, 3).to_f_vector # => Vector2d(2.0,3.0)
51
- #
52
- def to_f_vector
53
- self.class.new(x.to_f, y.to_f)
54
- end
55
-
56
- # Converts vector to string.
57
- #
58
- # Vector2d.new(150, 100).to_s # => "150x100"
59
- #
60
- def to_s
61
- "#{x}x#{y}"
62
- end
63
- end
64
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Vector2d
4
- module Fitting
5
- # Scales down the given vector unless it fits inside.
6
- #
7
- # vector = Vector2d(20, 20)
8
- # vector.contain(Vector2d(10, 10)) # => Vector2d(10,10)
9
- # vector.contain(Vector2d(40, 20)) # => Vector2d(20,10)
10
- # vector.contain(Vector2d(20, 40)) # => Vector2d(10,20)
11
- #
12
- def contain(other)
13
- v, = coerce(other)
14
- v.x > x || v.y > y ? other.fit(self) : other
15
- end
16
-
17
- # Scales the vector to fit inside another vector, retaining the
18
- # aspect ratio.
19
- #
20
- # vector = Vector2d(20, 10)
21
- # vector.fit(Vector2d(10, 10)) # => Vector2d(10,5)
22
- # vector.fit(Vector2d(20, 20)) # => Vector2d(20,10)
23
- # vector.fit(Vector2d(40, 40)) # => Vector2d(40,20)
24
- #
25
- # Note: Either axis will be disregarded if zero or nil. This is a
26
- # feature, not a bug.
27
- #
28
- def fit(other)
29
- v, = coerce(other)
30
- scale = v.to_f_vector / self
31
- self * (
32
- if scale.y.zero? || (scale.x.positive? && scale.x < scale.y)
33
- scale.x
34
- else
35
- scale.y
36
- end
37
- )
38
- end
39
- alias constrain_both fit
40
-
41
- # Constrain/expand so that one of the coordinates fit within (the
42
- # square implied by) another vector.
43
- #
44
- # constraint = Vector2d(5, 5)
45
- # Vector2d(20, 10).fit_either(constraint) # => Vector2d(10,5)
46
- # Vector2d(10, 20).fit_either(constraint) # => Vector2d(5,10)
47
- #
48
- def fit_either(other)
49
- v, = coerce(other)
50
- scale = v.to_f_vector / self
51
- if scale.x.positive? && scale.y.positive?
52
- scale = [scale.x, scale.y].max
53
- self * scale
54
- else
55
- fit(v)
56
- end
57
- end
58
- alias constrain_one fit_either
59
- end
60
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Vector2d
4
- module Properties
5
- # Angle of vector.
6
- #
7
- # Vector2d(2, 3).angle # => 0.9827..
8
- #
9
- def angle
10
- Math.atan2(y, x)
11
- end
12
-
13
- # Aspect ratio of vector.
14
- #
15
- # Vector2d(2, 3).aspect_ratio # => 0.6667..
16
- #
17
- def aspect_ratio
18
- (x.to_f / y).abs
19
- end
20
-
21
- # Length of vector.
22
- #
23
- # Vector2d(2, 3).length # => 3.6055..
24
- #
25
- def length
26
- Math.sqrt(squared_length)
27
- end
28
-
29
- # Squared length of vector.
30
- #
31
- # Vector2d(2, 3).squared_length # => 13
32
- #
33
- def squared_length
34
- (x * x) + (y * y)
35
- end
36
-
37
- # Is this a normalized vector?
38
- #
39
- # Vector2d(0, 1).normalized? # => true
40
- # Vector2d(2, 3).normalized? # => false
41
- #
42
- def normalized?
43
- (length.to_f - 1.0).abs < Float::EPSILON
44
- end
45
- end
46
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Vector2d
4
- module Transformations
5
- # Rounds vector to up nearest integer.
6
- #
7
- # Vector2d(2.4, 3.6).ceil # => Vector2d(3,4)
8
- #
9
- def ceil
10
- self.class.new(x.ceil, y.ceil)
11
- end
12
-
13
- # Clamps the vector between two others, one axis at a time.
14
- # The bounds are coerced, so scalars work too.
15
- #
16
- # vector = Vector2d(2, 8)
17
- # vector.clamp(Vector2d(3, 3), Vector2d(6, 6)) # => Vector2d(3,6)
18
- # vector.clamp(3, 6) # => Vector2d(3,6)
19
- #
20
- def clamp(min, max)
21
- min_v, = coerce(min)
22
- max_v, = coerce(max)
23
- self.class.new(x.clamp(min_v.x, max_v.x), y.clamp(min_v.y, max_v.y))
24
- end
25
-
26
- # Rounds vector to up nearest integer.
27
- #
28
- # Vector2d(2.4, 3.6).floor # => Vector2d(2,3)
29
- #
30
- def floor
31
- self.class.new(x.floor, y.floor)
32
- end
33
-
34
- # Normalizes the vector.
35
- #
36
- # vector = Vector2d(2, 3)
37
- # vector.normalize # => Vector2d(0.5547.., 0.8320..)
38
- # vector.normalize.length # => 1.0
39
- #
40
- def normalize
41
- resize(1.0)
42
- end
43
-
44
- # Returns a perpendicular vector.
45
- #
46
- # Vector2d(2, 3).perpendicular # => Vector2d(-3,2)
47
- #
48
- def perpendicular
49
- Vector2d.new(-y, x)
50
- end
51
-
52
- # Changes magnitude of vector.
53
- #
54
- # Vector2d(2, 3).resize(1.0) # => Vector2d(0.5547.., 0.8320..)
55
- #
56
- def resize(new_length)
57
- self * (new_length / length)
58
- end
59
-
60
- # Reverses the vector.
61
- #
62
- # Vector2d(2, 3).reverse # => Vector2d(-2,-3)
63
- #
64
- def reverse
65
- self.class.new(-x, -y)
66
- end
67
-
68
- # Rotates the vector
69
- #
70
- # Vector2d(1, 0).rotate(Math:PI/2) => Vector2d(1,0)
71
- #
72
- def rotate(angle)
73
- Vector2d.new(
74
- (x * Math.cos(angle)) - (y * Math.sin(angle)),
75
- (x * Math.sin(angle)) + (y * Math.cos(angle))
76
- )
77
- end
78
-
79
- # Rounds vector to nearest integer.
80
- #
81
- # Vector2d(2.4, 3.6).round # => Vector2d(2,4)
82
- # Vector2d(2.4444, 3.666).round(2) # => Vector2d(2.44, 3.67)
83
- #
84
- def round(digits = 0)
85
- self.class.new(x.round(digits), y.round(digits))
86
- end
87
-
88
- # Truncates to max length if vector is longer than max.
89
- #
90
- # vector = Vector2d(2.0, 3.0)
91
- # vector.truncate(5.0) # => Vector2d(2.0, 3.0)
92
- # vector.truncate(1.0) # => Vector2d(0.5547.., 0.8320..)
93
- #
94
- def truncate(max)
95
- resize([max, length].min)
96
- end
97
- end
98
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe Vector2d::Coercions do
6
- subject(:vector) { Vector2d.new(2, 3) }
7
-
8
- describe "#inspect" do
9
- it "renders a string representation" do
10
- expect(vector.inspect).to eq("Vector2d(2,3)")
11
- end
12
- end
13
-
14
- describe "#to_a" do
15
- it "returns an array" do
16
- expect(vector.to_a).to eq([2, 3])
17
- end
18
- end
19
-
20
- describe "#to_f_vector" do
21
- subject { vector.to_f_vector }
22
-
23
- its(:x) { is_expected.to be_a(Float) }
24
- its(:y) { is_expected.to be_a(Float) }
25
- end
26
-
27
- describe "#to_hash" do
28
- it "returns a hash" do
29
- expect(vector.to_hash).to eq(x: 2, y: 3)
30
- end
31
- end
32
-
33
- describe "#to_i_vector" do
34
- subject { vector.to_i_vector }
35
-
36
- let(:vector) { Vector2d.new(2.0, 3.0) }
37
-
38
- its(:x) { is_expected.to be_a(Integer) }
39
- its(:y) { is_expected.to be_a(Integer) }
40
- end
41
-
42
- describe "#to_s" do
43
- context "when fixnum" do
44
- subject(:vector) { Vector2d.new(2, 3) }
45
-
46
- it "renders a string" do
47
- expect(vector.to_s).to eq("2x3")
48
- end
49
- end
50
-
51
- context "when float" do
52
- subject(:vector) { Vector2d.new(2.0, 3.0) }
53
-
54
- it "renders a string" do
55
- expect(vector.to_s).to eq("2.0x3.0")
56
- end
57
- end
58
- end
59
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe Vector2d::Fitting do
6
- let(:original) { Vector2d.new(300, 300) }
7
-
8
- describe "#contain" do
9
- subject(:vector) { original.contain(comp) }
10
-
11
- context "when vector is smaller" do
12
- let(:comp) { Vector2d.new(150, 100) }
13
-
14
- its(:x) { is_expected.to eq(150) }
15
- its(:y) { is_expected.to eq(100) }
16
- end
17
-
18
- context "when vector is wider" do
19
- let(:comp) { Vector2d.new(400, 300) }
20
-
21
- its(:x) { is_expected.to eq(300) }
22
- its(:y) { is_expected.to eq(225) }
23
- end
24
-
25
- context "when vector is higher" do
26
- let(:comp) { Vector2d.new(300, 400) }
27
-
28
- its(:x) { is_expected.to eq(225) }
29
- its(:y) { is_expected.to eq(300) }
30
- end
31
- end
32
-
33
- describe "#fit" do
34
- subject(:vector) { original.fit(comp) }
35
-
36
- context "when scaling by height" do
37
- let(:comp) { Vector2d.new(200, 150) }
38
-
39
- its(:x) { is_expected.to eq(150) }
40
- its(:y) { is_expected.to eq(150) }
41
- end
42
-
43
- context "when scaling by width" do
44
- let(:comp) { Vector2d.new(150, 200) }
45
-
46
- its(:x) { is_expected.to eq(150) }
47
- its(:y) { is_expected.to eq(150) }
48
- end
49
- end
50
-
51
- describe "#fit_either" do
52
- subject(:vector) { original.fit_either(comp) }
53
-
54
- let(:original) { Vector2d.new(300, 300) }
55
-
56
- context "when width is largest" do
57
- let(:comp) { Vector2d.new(200, 150) }
58
-
59
- its(:x) { is_expected.to eq(200) }
60
- its(:y) { is_expected.to eq(200) }
61
- end
62
-
63
- context "when height is largest" do
64
- let(:comp) { Vector2d.new(150, 200) }
65
-
66
- its(:x) { is_expected.to eq(200) }
67
- its(:y) { is_expected.to eq(200) }
68
- end
69
- end
70
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe Vector2d::Properties do
6
- subject(:vector) { Vector2d.new(2, 3) }
7
-
8
- describe "#angle" do
9
- it "returns the angle" do
10
- expect(vector.angle).to be_within(0.0001).of(0.9827)
11
- end
12
- end
13
-
14
- describe "#aspect_ratio" do
15
- it "returns the aspect_ratio" do
16
- expect(vector.aspect_ratio).to be_within(0.0001).of(0.6667)
17
- end
18
- end
19
-
20
- describe "#length" do
21
- it "calculates the length" do
22
- expect(vector.length).to be_within(0.0001).of(3.6055)
23
- end
24
- end
25
-
26
- describe "#squared_length" do
27
- it "calculates the squared length" do
28
- expect(vector.squared_length).to eq(13)
29
- end
30
- end
31
-
32
- describe "#normalized?" do
33
- subject { vector.normalized? }
34
-
35
- context "when vector is normalized" do
36
- let(:vector) { Vector2d.new(2, 3).normalize }
37
-
38
- it { is_expected.to be(true) }
39
- end
40
-
41
- context "when vector isn't normalized" do
42
- let(:vector) { Vector2d.new(2, 3) }
43
-
44
- it { is_expected.to be(false) }
45
- end
46
- end
47
- end
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe Vector2d::Transformations do
6
- subject(:vector) { Vector2d.new(2, 3) }
7
-
8
- describe "#ceil" do
9
- subject(:vector) { Vector2d.new(2.3, 3.3) }
10
-
11
- it "rounds the vector up" do
12
- expect(vector.ceil).to eq(Vector2d.new(3, 4))
13
- end
14
- end
15
-
16
- describe "#clamp" do
17
- subject(:vector) { Vector2d.new(2, 8) }
18
-
19
- it "clamps each axis" do
20
- expect(vector.clamp(Vector2d.new(3, 3), Vector2d.new(6, 6)))
21
- .to eq(Vector2d.new(3, 6))
22
- end
23
-
24
- it "coerces the bounds" do
25
- expect(vector.clamp(3, 6)).to eq(Vector2d.new(3, 6))
26
- end
27
-
28
- it "leaves a vector within the bounds alone" do
29
- expect(vector.clamp(0, 10)).to eq(vector)
30
- end
31
- end
32
-
33
- describe "#floor" do
34
- subject(:vector) { Vector2d.new(2.7, 3.6) }
35
-
36
- it "rounds the vector down" do
37
- expect(vector.floor).to eq(Vector2d.new(2, 3))
38
- end
39
- end
40
-
41
- describe "#normalize" do
42
- subject { vector.normalize }
43
-
44
- its(:x) { is_expected.to be_within(0.0001).of(0.5547) }
45
- its(:y) { is_expected.to be_within(0.0001).of(0.8320) }
46
- end
47
-
48
- describe "#perpendicular" do
49
- it "returns a perpendicular vector" do
50
- expect(vector.perpendicular).to eq(Vector2d.new(-3, 2))
51
- end
52
- end
53
-
54
- describe "#resize" do
55
- subject(:resized) { vector.resize(2.0) }
56
-
57
- it "modifies the vector length" do
58
- expect(resized.length).to be_within(0.0001).of(2.0)
59
- end
60
-
61
- it "modifies the x property" do
62
- expect(resized.x).to be_within(0.0001).of(1.1094)
63
- end
64
-
65
- it "modifies the y property" do
66
- expect(resized.y).to be_within(0.0001).of(1.6641)
67
- end
68
- end
69
-
70
- describe "#reverse" do
71
- it "reverses the vector" do
72
- expect(vector.reverse).to eq(Vector2d.new(-2, -3))
73
- end
74
- end
75
-
76
- describe "#rotate" do
77
- subject { vector.rotate(rotation).round(3) }
78
-
79
- let(:vector) { Vector2d.new(1, 0) }
80
-
81
- context "when roating by PI" do
82
- let(:rotation) { Math::PI }
83
-
84
- it { is_expected.to eq(Vector2d.new(-1, 0)) }
85
- end
86
-
87
- context "when roating by PI/2" do
88
- let(:rotation) { Math::PI / 2 }
89
-
90
- it { is_expected.to eq(Vector2d.new(0, 1)) }
91
- end
92
-
93
- context "when roating by -PI/2" do
94
- let(:rotation) { -Math::PI / 2 }
95
-
96
- it { is_expected.to eq(Vector2d.new(0, -1)) }
97
- end
98
-
99
- context "when roating by PI/4" do
100
- let(:rotation) { Math::PI / 4 }
101
-
102
- it { is_expected.to eq(Vector2d.new(0.707, 0.707)) }
103
- end
104
- end
105
-
106
- describe "#round" do
107
- let(:vector) { Vector2d.new(2.3333, 3.666) }
108
-
109
- context "without argument" do
110
- subject { vector.round }
111
-
112
- it { is_expected.to eq(Vector2d.new(2, 4)) }
113
- end
114
-
115
- context "with precision" do
116
- subject { vector.round(2) }
117
-
118
- it { is_expected.to eq(Vector2d.new(2.33, 3.67)) }
119
- end
120
- end
121
-
122
- describe "#truncate" do
123
- context "when argument is longer than length" do
124
- let(:arg) { 5.0 }
125
-
126
- it "does not change the length" do
127
- expect(vector.truncate(arg).length).to be_within(0.0001).of(3.6055)
128
- end
129
- end
130
-
131
- context "when argument is shorter than length" do
132
- let(:arg) { 2.5 }
133
-
134
- it "changes the length" do
135
- expect(vector.truncate(arg).length).to be_within(0.0001).of(arg)
136
- end
137
- end
138
- end
139
- end