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
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Vector2d
4
+ # Products of two vectors, and the projection, reflection and
5
+ # refraction built on them.
6
+ module Projection
7
+ # The products taking both vectors as arguments. Extended into
8
+ # Vector2d, so they are called on the class.
9
+ #
10
+ # Vector2d.dot_product(Vector2d(2, 1), Vector2d(2, 3)) # => 7
11
+ #
12
+ module ClassMethods
13
+ # Calculates dot product of two vectors.
14
+ #
15
+ # v1 = Vector2d(2, 1)
16
+ # v2 = Vector2d(2, 3)
17
+ # Vector2d.dot_product(v1, v2) # => 7
18
+ #
19
+ # @param vector1 [Vector2d] one of the vectors
20
+ # @param vector2 [Vector2d] the other vector
21
+ # @return [Integer, Float, Rational, BigDecimal] a scalar
22
+ def dot_product(vector1, vector2)
23
+ (vector1.x * vector2.x) + (vector1.y * vector2.y)
24
+ end
25
+
26
+ # Calculates cross product of two vectors.
27
+ #
28
+ # v1 = Vector2d(2, 1)
29
+ # v2 = Vector2d(2, 3)
30
+ # Vector2d.cross_product(v1, v2) # => 4
31
+ #
32
+ # @param vector1 [Vector2d] the vector the product is measured from
33
+ # @param vector2 [Vector2d] the vector the product is measured to
34
+ # @return [Integer, Float, Rational, BigDecimal] a scalar
35
+ def cross_product(vector1, vector2)
36
+ (vector1.x * vector2.y) - (vector1.y * vector2.x)
37
+ end
38
+ end
39
+
40
+ # Dot product of this vector and another vector.
41
+ #
42
+ # v1 = Vector2d(2, 1)
43
+ # v2 = Vector2d(2, 3)
44
+ # v1.dot_product(v2) # => 7
45
+ #
46
+ # @!macro coercible
47
+ # @return [Integer, Float, Rational, BigDecimal] a scalar
48
+ def dot_product(other)
49
+ v = coerce_vector(other)
50
+ self.class.dot_product(self, v)
51
+ end
52
+ alias inner_product dot_product
53
+ alias dot dot_product
54
+
55
+ # Cross product of this vector and another vector. In two
56
+ # dimensions this is a scalar, the z component of the equivalent
57
+ # three dimensional cross product. Vector#cross_product returns a
58
+ # perpendicular vector instead, which is #perpendicular here.
59
+ #
60
+ # v1 = Vector2d(2, 1)
61
+ # v2 = Vector2d(2, 3)
62
+ # v1.cross_product(v2) # => 4
63
+ #
64
+ # @!macro coercible
65
+ # @return [Integer, Float, Rational, BigDecimal] a scalar
66
+ def cross_product(other)
67
+ v = coerce_vector(other)
68
+ self.class.cross_product(self, v)
69
+ end
70
+
71
+ # Vector projection of this vector onto another vector. The
72
+ # argument is coerced, so scalars work too.
73
+ #
74
+ # v1 = Vector2d(2, 3)
75
+ # v2 = Vector2d(4, 0)
76
+ # v1.project(v2) # => Vector2d(2.0,0.0)
77
+ #
78
+ # The zero vector has no direction, and there is nothing to project
79
+ # onto. The zero vector is returned.
80
+ #
81
+ # v1.project(Vector2d(0, 0)) # => Vector2d(0.0,0.0)
82
+ #
83
+ # @!macro coercible
84
+ # @return [self]
85
+ def project(other)
86
+ v = coerce_vector(other)
87
+ return build(0.0, 0.0) if v.zero?
88
+
89
+ scale = dot_product(v).to_f / v.length_squared
90
+ build(v.x * scale, v.y * scale)
91
+ end
92
+
93
+ # Vector rejection of this vector from another vector, the component
94
+ # left over when the projection is subtracted.
95
+ #
96
+ # v1 = Vector2d(2, 3)
97
+ # v2 = Vector2d(4, 0)
98
+ # v1.reject(v2) # => Vector2d(0.0,3.0)
99
+ #
100
+ # The zero vector has no direction, and nothing is projected away.
101
+ #
102
+ # v1.reject(Vector2d(0, 0)) # => Vector2d(2.0,3.0)
103
+ #
104
+ # @!macro coercible
105
+ # @return [self]
106
+ def reject(other)
107
+ self - project(other)
108
+ end
109
+
110
+ # Scalar projection of this vector onto another vector, the signed
111
+ # length of the projection. It is negative when the vectors point in
112
+ # opposite directions.
113
+ #
114
+ # v1 = Vector2d(2, 3)
115
+ # v2 = Vector2d(4, 0)
116
+ # v1.scalar_projection(v2) # => 2.0
117
+ # v1.scalar_projection(Vector2d(-4, 0)) # => -2.0
118
+ #
119
+ # The zero vector has no direction, and there is nothing to project
120
+ # onto. The scalar projection is zero.
121
+ #
122
+ # v1.scalar_projection(Vector2d(0, 0)) # => 0.0
123
+ #
124
+ # @!macro coercible
125
+ # @return [Float] a scalar, the signed length of the projection
126
+ def scalar_projection(other)
127
+ v = coerce_vector(other)
128
+ return 0.0 if v.zero?
129
+
130
+ dot_product(v) / v.length
131
+ end
132
+
133
+ # Reflects this vector about the line perpendicular to the normal,
134
+ # the way a ray bounces off a surface. The normal is normalized
135
+ # internally, so it can be of any length.
136
+ #
137
+ # vector = Vector2d(2, 3)
138
+ # vector.reflect(Vector2d(0, 1)) # => Vector2d(2.0,-3.0)
139
+ # vector.reflect(Vector2d(0, 5)) # => Vector2d(2.0,-3.0)
140
+ #
141
+ # The zero vector has no direction, and defines no surface to
142
+ # reflect off. Nothing is reflected, and the vector is returned.
143
+ #
144
+ # vector.reflect(Vector2d(0, 0)) # => Vector2d(2.0,3.0)
145
+ #
146
+ # @!macro coercible
147
+ # @return [self]
148
+ def reflect(normal)
149
+ v = coerce_vector(normal)
150
+ return to_f_vector if v.zero?
151
+
152
+ n = v.normalize
153
+ self - (n * (2 * dot_product(n)))
154
+ end
155
+
156
+ # Refracts this vector through a surface with the given normal and
157
+ # ratio of refractive indices, the way a ray bends entering a
158
+ # different medium. The normal is normalized internally, so it can
159
+ # be of any length.
160
+ #
161
+ # ray = Vector2d(1, -1).normalize
162
+ # ray.refract(Vector2d(0, 1), 0.5) # => Vector2d(0.3535..,-0.9354..)
163
+ # ray.refract(Vector2d(0, 5), 0.5) # => Vector2d(0.3535..,-0.9354..)
164
+ #
165
+ # A ratio of one leaves the ray on its course.
166
+ #
167
+ # ray.refract(Vector2d(0, 1), 1.0) # => Vector2d(0.7071..,-0.7071..)
168
+ #
169
+ # Past the critical angle the ray does not cross the surface at all.
170
+ # This is total internal reflection, and the zero vector is
171
+ # returned.
172
+ #
173
+ # ray.refract(Vector2d(0, 1), 2.0) # => Vector2d(0.0,0.0)
174
+ #
175
+ # The zero vector has no direction, and defines no surface to
176
+ # refract through. Nothing is refracted, and the vector is returned.
177
+ #
178
+ # ray.refract(Vector2d(0, 0), 0.5) # => Vector2d(0.7071..,-0.7071..)
179
+ #
180
+ # Raises ArgumentError unless the ratio is a real number.
181
+ #
182
+ # ray.refract(Vector2d(0, 1), Complex(1, 2)) # => ArgumentError
183
+ #
184
+ # @!macro coercible
185
+ # @param refractive_index [Integer, Float, Rational, BigDecimal]
186
+ # the ratio of refractive indices
187
+ # @return [self]
188
+ def refract(normal, refractive_index)
189
+ v = coerce_vector(normal)
190
+ eta = coordinate(refractive_index)
191
+ return to_f_vector if v.zero?
192
+
193
+ refract_through(v.normalize, eta)
194
+ end
195
+
196
+ private
197
+
198
+ # Refracts through a normalized normal. A negative discriminant is
199
+ # total internal reflection, where no refracted ray exists.
200
+ def refract_through(normal, eta)
201
+ cosine = dot_product(normal)
202
+ k = 1 - ((eta * eta) * (1 - (cosine * cosine)))
203
+ return build(0.0, 0.0) if k.negative?
204
+
205
+ (self * eta) - (normal * ((eta * cosine) + Math.sqrt(k)))
206
+ end
207
+ end
208
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Vector2d
4
- VERSION = "2.3.0"
4
+ # The version of the gem.
5
+ VERSION = "3.0.0"
5
6
  end
data/lib/vector2d.rb CHANGED
@@ -1,86 +1,198 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "vector2d/calculations"
4
- require "vector2d/coercions"
5
- require "vector2d/fitting"
6
- require "vector2d/properties"
7
- require "vector2d/transformations"
8
- require "vector2d/version"
9
-
10
- class Vector2d
11
- extend Vector2d::Calculations::ClassMethods
12
- include Vector2d::Calculations
13
- include Vector2d::Coercions
14
- include Vector2d::Fitting
15
- include Vector2d::Properties
16
- include Vector2d::Transformations
17
-
18
- class << self
19
- # Creates a new vector.
20
- # The following examples are all valid:
21
- #
22
- # Vector2d.parse(150, 100)
23
- # Vector2d.parse(150.0, 100.0)
24
- # Vector2d.parse("150x100")
25
- # Vector2d.parse("150.0x100.0")
26
- # Vector2d.parse([150,100})
27
- # Vector2d.parse({x: 150, y: 100})
28
- # Vector2d.parse({"x" => 150.0, "y" => 100.0})
29
- # Vector2d.parse(Vector2d(150, 100))
30
- def parse(arg, second_arg = nil)
31
- if second_arg.nil?
32
- parse_single_arg(arg)
33
- else
34
- new(arg, second_arg)
35
- end
36
- end
37
-
38
- private
39
-
40
- def parse_single_arg(arg)
41
- return arg if arg.is_a?(Vector2d)
42
- return parse(*arg) if arg.is_a?(Array)
43
- return parse_str(arg) if arg.is_a?(String)
44
- return parse_hash(arg.dup) if arg.is_a?(Hash)
45
-
46
- new(arg, arg)
47
- end
3
+ # The one place the types Vector2d.parse accepts are written out. Every
4
+ # parameter that is coerced into a vector takes this union.
5
+ #
6
+ # @!macro [new] coercible
7
+ # @param $1 [Vector2d, Array, String, Hash, Integer, Float, Rational,
8
+ # BigDecimal, ::Vector, ::Matrix] anything Vector2d.parse accepts
48
9
 
49
- def parse_hash(hash)
50
- hash[:x] ||= hash["x"] if hash.key?("x")
51
- hash[:y] ||= hash["y"] if hash.key?("y")
52
- new(hash[:x], hash[:y])
53
- end
10
+ require_relative "vector2d/angles"
11
+ require_relative "vector2d/arithmetic"
12
+ require_relative "vector2d/comparison"
13
+ require_relative "vector2d/componentwise"
14
+ require_relative "vector2d/constructors"
15
+ require_relative "vector2d/conversions"
16
+ require_relative "vector2d/coordinates"
17
+ require_relative "vector2d/deprecation"
18
+ require_relative "vector2d/dimensions"
19
+ require_relative "vector2d/interpolation"
20
+ require_relative "vector2d/lengths"
21
+ require_relative "vector2d/matrix_interop"
22
+ require_relative "vector2d/parsing"
23
+ require_relative "vector2d/projection"
24
+ require_relative "vector2d/version"
54
25
 
55
- def parse_str(str)
56
- raise ArgumentError, "not a valid string input" unless /^\s*[\d.]*\s*x\s*[\d.]*\s*$/.match?(str)
26
+ # An immutable two dimensional vector.
27
+ #
28
+ # Coordinates are real numbers, and keep the type they were given.
29
+ # Integer coordinates stay exact through arithmetic with integers, and
30
+ # widen to floats when a float is involved.
31
+ #
32
+ # Vector2d(2, 3) * 2 # => Vector2d(4,6)
33
+ # Vector2d(2, 3) * 0.5 # => Vector2d(1.0,1.5)
34
+ #
35
+ # Instances are frozen, so every operation returns a new vector instead
36
+ # of changing the receiver. New vectors are built through #build, which
37
+ # a subclass overrides to carry its own state across operations.
38
+ #
39
+ # .parse is the permissive constructor, and Vector2d() is shorthand for
40
+ # it. .new takes exactly two coordinates.
41
+ #
42
+ # Vector2d("2x3") # => Vector2d(2,3)
43
+ # Vector2d.new(2, 3) # => Vector2d(2,3)
44
+ #
45
+ class Vector2d
46
+ extend Vector2d::Angles::ClassMethods
47
+ extend Vector2d::Constructors
48
+ extend Vector2d::Coordinates
49
+ extend Vector2d::Parsing
50
+ extend Vector2d::Projection::ClassMethods
51
+ include Vector2d::Angles
52
+ include Vector2d::Arithmetic
53
+ include Vector2d::Comparison
54
+ include Vector2d::Componentwise
55
+ include Vector2d::Conversions
56
+ include Vector2d::Coordinates
57
+ include Vector2d::Deprecation
58
+ include Vector2d::Dimensions
59
+ include Vector2d::Interpolation
60
+ include Vector2d::Lengths
61
+ include Vector2d::MatrixInterop
62
+ include Vector2d::Projection
57
63
 
58
- x, y = str.split("x")
59
- new(x.to_f, y.to_f)
60
- end
64
+ # Builds a new vector of this class from two coordinates, the way
65
+ # .parse and .from_angle do. Override it in a subclass whose
66
+ # constructor requires more than the coordinates, and see #build for
67
+ # the instance side.
68
+ #
69
+ # class Labeled < Vector2d
70
+ # attr_reader :label
71
+ #
72
+ # def initialize(x, y, label = nil)
73
+ # @label = label
74
+ # super(x, y)
75
+ # end
76
+ #
77
+ # def self.build(x, y) = new(x, y, "unlabeled")
78
+ # end
79
+ #
80
+ # Labeled.parse("2x3").label # => "unlabeled"
81
+ #
82
+ # @param x [Integer, Float, Rational, BigDecimal] the x coordinate
83
+ # @param y [Integer, Float, Rational, BigDecimal] the y coordinate
84
+ # @return [Vector2d] a vector of this class
85
+ def self.build(x, y)
86
+ new(x, y)
61
87
  end
62
88
 
89
+ # The coordinates.
90
+ #
91
+ # Vector2d(2, 3).x # => 2
92
+ # Vector2d(2, 3).y # => 3
93
+ #
94
+ # @return [Integer, Float, Rational, BigDecimal]
63
95
  attr_reader :x, :y
64
96
 
97
+ # Creates a vector from two coordinates, which must be real numbers.
98
+ # Every vector is constructed through here, so this is what keeps a
99
+ # coordinate from being anything else. Instances are frozen.
100
+ #
101
+ # Vector2d.new(2, 3) # => Vector2d(2,3)
102
+ # Vector2d.new(Complex(1, 2), 3) # => ArgumentError
103
+ # Vector2d.new(2, 3).frozen? # => true
104
+ # Ractor.shareable?(Vector2d.new(2, 3)) # => true
105
+ #
106
+ # @param x [Integer, Float, Rational, BigDecimal] the x coordinate
107
+ # @param y [Integer, Float, Rational, BigDecimal] the y coordinate
65
108
  def initialize(x, y)
66
- @x = x
67
- @y = y
109
+ @x = coordinate(x)
110
+ @y = coordinate(y)
111
+ freeze
112
+ end
113
+
114
+ # Copies are frozen too.
115
+ #
116
+ # Vector2d(2, 3).dup.frozen? # => true
117
+ #
118
+ # @param other [Vector2d] the vector being copied
119
+ # @return [void]
120
+ def initialize_copy(other)
121
+ super
122
+ freeze
68
123
  end
69
124
 
70
125
  # Compares two vectors
71
126
  #
72
127
  # Vector2d(2, 3) == Vector2d(2, 3) # => true
73
128
  # Vector2d(2, 3) == Vector2d(1, 0) # => false
129
+ # Vector2d(2, 3) == [2, 3] # => false
74
130
  #
131
+ # @param other [Object] any object
132
+ # @return [Boolean]
75
133
  def ==(other)
76
- other.x == x && other.y == y
134
+ other.is_a?(Vector2d) && other.x == x && other.y == y
135
+ end
136
+
137
+ # Compares two vectors for hash equality. Unlike #==, the other object
138
+ # must be a vector of the same class, and the coordinates must be of
139
+ # the same type.
140
+ #
141
+ # Vector2d(2, 3).eql?(Vector2d(2, 3)) # => true
142
+ # Vector2d(2, 3).eql?(Vector2d(2.0, 3.0)) # => false
143
+ #
144
+ # @param other [Object] any object
145
+ # @return [Boolean]
146
+ def eql?(other)
147
+ other.instance_of?(self.class) && x.eql?(other.x) && y.eql?(other.y)
148
+ end
149
+
150
+ # Hash value of the vector, consistent with #eql?.
151
+ #
152
+ # Vector2d(2, 3).hash == Vector2d(2, 3).hash # => true
153
+ #
154
+ # @return [Integer]
155
+ def hash
156
+ [self.class, x, y].hash
157
+ end
158
+
159
+ # Builds a new vector of this class from two coordinates. Every method
160
+ # that returns a new vector goes through here, so a subclass whose
161
+ # constructor takes more than the coordinates only has to override
162
+ # this to have its own state carried across operations.
163
+ #
164
+ # class Labeled < Vector2d
165
+ # attr_reader :label
166
+ #
167
+ # def initialize(x, y, label = nil)
168
+ # @label = label
169
+ # super(x, y)
170
+ # end
171
+ #
172
+ # def build(x, y) = self.class.new(x, y, label)
173
+ # end
174
+ #
175
+ # Labeled.new(2, 3, "point").abs.label # => "point"
176
+ #
177
+ # Class level constructors have no instance to carry state from, and
178
+ # use .build instead.
179
+ #
180
+ # @param x [Integer, Float, Rational, BigDecimal] the x coordinate
181
+ # @param y [Integer, Float, Rational, BigDecimal] the y coordinate
182
+ # @return [self]
183
+ def build(x, y)
184
+ self.class.build(x, y)
77
185
  end
78
186
  end
79
187
 
80
- # Instantiates a Vector2d
188
+ # Shorthand for Vector2d.parse, and takes the same arguments.
81
189
  #
82
- # Vector2d(2, 3) # => Vector2d(2,3)
190
+ # Vector2d(2, 3) # => Vector2d(2,3)
191
+ # Vector2d("2x3") # => Vector2d(2,3)
192
+ # Vector2d([2, 3]) # => Vector2d(2,3)
83
193
  #
194
+ # @return [Vector2d]
195
+ # @see Vector2d.parse for the arguments
84
196
  def Vector2d(*)
85
197
  Vector2d.parse(*)
86
198
  end