sevgi-geometry 0.73.2 → 0.94.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.
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Geometry
5
+ # Generated superclass for Parallelogram.
6
+ # @api private
7
+ ParallelogramBase = Element.lined(4)
8
+ private_constant :ParallelogramBase
9
+
10
+ # Closed four-sided element built from horizontal and vertical segments.
11
+ class Parallelogram < ParallelogramBase
12
+ # Builds a parallelogram from adjacent horizontal and vertical segments.
13
+ # @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
14
+ # @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
15
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
16
+ # @return [Sevgi::Geometry::Parallelogram]
17
+ # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
18
+ def self.[](horizontal, vertical, position: Origin)
19
+ horizontal, vertical = Tuples[Segment, horizontal, vertical]
20
+
21
+ new_by_segments(horizontal, vertical.reverse, horizontal.reverse, vertical, position:)
22
+ end
23
+
24
+ # Builds a parallelogram from a horizontal segment and tallness constraint.
25
+ # @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
26
+ # @param tallness [Sevgi::Geometry::Polar, Array<Numeric>] target tallness as length and angle
27
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
28
+ # @return [Sevgi::Geometry::Parallelogram]
29
+ # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
30
+ def self.new_by_height(horizontal:, tallness:, position: Origin)
31
+ horizontal = Tuple[Segment, horizontal]
32
+ tallness = Tuple[Polar, tallness]
33
+
34
+ height = tallness.length - horizontal.y.abs
35
+ angle = tallness.angle
36
+ length = height / F.sin(angle)
37
+
38
+ self[horizontal, Segment[length, angle], position:]
39
+ end
40
+
41
+ # Builds a parallelogram from a vertical segment and wideness constraint.
42
+ # @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
43
+ # @param wideness [Sevgi::Geometry::Polar, Array<Numeric>] target wideness as length and angle
44
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
45
+ # @return [Sevgi::Geometry::Parallelogram]
46
+ # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
47
+ def self.new_by_width(vertical:, wideness:, position: Origin)
48
+ vertical = Tuple[Segment, vertical]
49
+ wideness = Tuple[Polar, wideness]
50
+
51
+ width = wideness.length - vertical.x.abs
52
+ angle = wideness.angle
53
+ length = width / F.cos(angle)
54
+
55
+ self[Segment[length, angle], vertical, position:]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,7 +2,13 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
- class Polygon < Element.lined
5
+ # Generated superclass for Polygon.
6
+ # @api private
7
+ PolygonBase = Element.lined
8
+ private_constant :PolygonBase
9
+
10
+ # Variable-size closed lined element.
11
+ class Polygon < PolygonBase
6
12
  end
7
13
  end
8
14
  end
@@ -2,7 +2,13 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
- class Polyline < Element.lined(open: true)
5
+ # Generated superclass for Polyline.
6
+ # @api private
7
+ PolylineBase = Element.lined(open: true)
8
+ private_constant :PolylineBase
9
+
10
+ # Variable-size open lined element.
11
+ class Polyline < PolylineBase
6
12
  end
7
13
  end
8
14
  end
@@ -2,8 +2,36 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
- class Rect < Element.lined(4)
6
- def self.call(top_left, bottom_right)
5
+ # Generated superclass for Rect.
6
+ # @api private
7
+ RectBase = Element.lined(4)
8
+ private_constant :RectBase
9
+
10
+ # Closed four-sided rectangle aligned to the screen axes.
11
+ class Rect < RectBase
12
+ # @overload [](width, height, position: Origin)
13
+ # Builds a rectangle from size and top-left position.
14
+ # @param width [Numeric] rectangle width
15
+ # @param height [Numeric] rectangle height
16
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
17
+ # @return [Sevgi::Geometry::Rect]
18
+ # @raise [Sevgi::Geometry::Error] when position cannot be coerced
19
+ def self.[](...) = from_size(...)
20
+
21
+ # @overload call(top_left, bottom_right)
22
+ # Builds a rectangle from two opposite corners.
23
+ # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
24
+ # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
25
+ # @return [Sevgi::Geometry::Rect]
26
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
27
+ def self.call(...) = from_corners(...)
28
+
29
+ # Builds a rectangle from two opposite corners.
30
+ # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
31
+ # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
32
+ # @return [Sevgi::Geometry::Rect]
33
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
34
+ def self.from_corners(top_left, bottom_right)
7
35
  top_left, bottom_right = Tuples[Point, top_left, bottom_right]
8
36
  width = (bottom_right.x - top_left.x).abs
9
37
 
@@ -15,35 +43,87 @@ module Sevgi
15
43
  )
16
44
  end
17
45
 
18
- def self.[](width, height, position: Origin)
46
+ # Builds a rectangle from size and top-left position.
47
+ # @param width [Numeric] rectangle width
48
+ # @param height [Numeric] rectangle height
49
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
50
+ # @return [Sevgi::Geometry::Rect]
51
+ # @raise [Sevgi::Geometry::Error] when position cannot be coerced
52
+ def self.from_size(width, height, position: Origin)
19
53
  new_by_segments(
20
- Segment.horizontal(width),
21
- Segment.vertical(height),
22
- Segment.horizontal!(width),
23
- Segment.vertical!(height),
54
+ Segment.rightward(width),
55
+ Segment.downward(height),
56
+ Segment.leftward(width),
57
+ Segment.upward(height),
24
58
  position:
25
59
  )
26
60
  end
27
61
 
62
+ # Draws the rectangle into a graphics node.
63
+ # @param node [Object] graphics node receiving the drawing command
64
+ # @return [Object] graphics node command result
28
65
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)
29
66
 
67
+ # Returns rectangle height.
68
+ # @return [Float]
30
69
  def height = @height ||= segments[1].length
31
70
 
71
+ # Returns rectangle width.
72
+ # @return [Float]
32
73
  def width = @width ||= segments[0].length
33
74
 
75
+ # @!parse
76
+ # # Returns the top-left corner.
77
+ # # @return [Sevgi::Geometry::Point]
78
+ # def top_left; end
79
+ #
80
+ # # Returns the top-right corner.
81
+ # # @return [Sevgi::Geometry::Point]
82
+ # def top_right; end
83
+ #
84
+ # # Returns the bottom-right corner.
85
+ # # @return [Sevgi::Geometry::Point]
86
+ # def bottom_right; end
87
+ #
88
+ # # Returns the bottom-left corner.
89
+ # # @return [Sevgi::Geometry::Point]
90
+ # def bottom_left; end
34
91
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
35
92
  define_method(corner) { points[i] }
36
93
  end
37
94
 
95
+ # @!parse
96
+ # # Returns the top side line.
97
+ # # @return [Sevgi::Geometry::Line]
98
+ # def top; end
99
+ #
100
+ # # Returns the right side line.
101
+ # # @return [Sevgi::Geometry::Line]
102
+ # def right; end
103
+ #
104
+ # # Returns the bottom side line.
105
+ # # @return [Sevgi::Geometry::Line]
106
+ # def bottom; end
107
+ #
108
+ # # Returns the left side line.
109
+ # # @return [Sevgi::Geometry::Line]
110
+ # def left; end
38
111
  %i[top right bottom left].each_with_index do |side, i|
39
112
  define_method(side) { lines[i] }
40
113
  end
41
114
  end
42
115
 
116
+ # Rectangle with equal width and height.
43
117
  class Square < Rect
118
+ # @return [Float] side length
44
119
  alias length width
45
120
 
46
- def self.[](length, position: Origin) = super(length, length, position:)
121
+ # Builds a square from side length and top-left position.
122
+ # @param length [Numeric] side length
123
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
124
+ # @return [Sevgi::Geometry::Square]
125
+ # @raise [Sevgi::Geometry::Error] when position cannot be coerced
126
+ def self.[](length, position: Origin) = from_size(length, length, position:)
47
127
  end
48
128
  end
49
129
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Geometry
5
+ # Generated superclass for Triangle.
6
+ # @api private
7
+ TriangleBase = Element.lined(3)
8
+ private_constant :TriangleBase
9
+
10
+ # Closed three-sided element built from two non-collinear adjacent segments.
11
+ class Triangle < TriangleBase
12
+ # Builds a triangle from two adjacent segments.
13
+ #
14
+ # The closing segment is the direct vector from the end of `segment_b`
15
+ # back to `position`. Segment order controls orientation; reversing the
16
+ # inputs returns the corresponding opposite orientation. Zero-length or
17
+ # collinear inputs are rejected using the current numeric precision.
18
+ # @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first segment
19
+ # @param segment_b [Sevgi::Geometry::Segment, Array<Numeric>] second segment
20
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
21
+ # @return [Sevgi::Geometry::Triangle]
22
+ # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced, or the segments are degenerate
23
+ def self.[](segment_a, segment_b, position: Origin)
24
+ a, b = Tuples[Segment, segment_a, segment_b]
25
+
26
+ validate!(a, b)
27
+ new_by_segments(a, b, closing_segment(a, b), position:)
28
+ end
29
+
30
+ def self.closing_segment(a, b)
31
+ Segment.(b.ending(a.ending(Origin)), Origin)
32
+ end
33
+
34
+ def self.cross(a, b) = (a.x * b.y) - (a.y * b.x)
35
+
36
+ def self.validate!(a, b)
37
+ if F.zero?(a.length) ||
38
+ F.zero?(b.length) ||
39
+ F.zero?(cross(a, b))
40
+ Error.("Triangle segments must form a non-degenerate triangle")
41
+ end
42
+ end
43
+
44
+ private_class_method :closing_segment, :cross, :validate!
45
+ end
46
+ end
47
+ end
@@ -3,31 +3,79 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  class Equation
6
+ # Base class for linear equations.
6
7
  class Linear < Equation
8
+ # Non-axis-aligned linear equation in `y = slope * x + intercept` form.
7
9
  class Diagonal < Linear
10
+ # @!attribute [r] slope
11
+ # @return [Float] line slope
12
+ # @!attribute [r] intercept
13
+ # @return [Float] y-intercept
8
14
  attr_reader :slope, :intercept
9
15
 
16
+ # Creates a diagonal linear equation.
17
+ # @param slope [Numeric] line slope
18
+ # @param intercept [Numeric] y-intercept
19
+ # @return [void]
20
+ # @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
10
21
  def initialize(slope:, intercept:)
11
22
  super()
12
23
 
13
- @slope = slope.to_f
14
- @intercept = intercept.to_f
24
+ @slope = Real[:slope, slope]
25
+ @intercept = Real[:intercept, intercept]
15
26
  end
16
27
 
28
+ # Returns an equation rounded to precision.
29
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
30
+ # @return [Sevgi::Geometry::Equation::Linear::Diagonal]
17
31
  def approx(precision = nil)
18
32
  self.class.new(slope: F.approx(slope, precision), intercept: F.approx(intercept, precision))
19
33
  end
20
34
 
35
+ # Reports strict equation equality.
36
+ # @param other [Object] object to compare
37
+ # @return [Boolean]
21
38
  def eql?(other) = self.class == other.class && [slope, intercept] == [other.slope, other.intercept]
22
39
 
40
+ # Returns a hash compatible with strict equality.
41
+ # @return [Integer]
23
42
  def hash = [self.class, slope, intercept].hash
24
43
 
25
- def left?(point) = F.gt?(point.y, y(point.x))
44
+ # Reports whether a point is on the left side of the line in screen coordinates.
45
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
46
+ # @return [Boolean]
47
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
48
+ def left?(point)
49
+ point = Tuple[Point, point]
26
50
 
27
- def on?(point) = F.eq?(point.y, y(point.x))
51
+ F.gt?(point.y, y(point.x))
52
+ end
53
+
54
+ # Reports whether a point is on the line.
55
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
56
+ # @return [Boolean]
57
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
58
+ def on?(point)
59
+ point = Tuple[Point, point]
60
+
61
+ F.eq?(point.y, y(point.x))
62
+ end
63
+
64
+ # Reports whether a point is on the right side of the line in screen coordinates.
65
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
66
+ # @return [Boolean]
67
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
68
+ def right?(point)
69
+ point = Tuple[Point, point]
28
70
 
29
- def right?(point) = F.lt?(point.y, y(point.x))
71
+ F.lt?(point.y, y(point.x))
72
+ end
30
73
 
74
+ # Returns a parallel equation shifted by a signed perpendicular offset.
75
+ # @param distance [Numeric, nil] signed perpendicular offset
76
+ # @param dx [Numeric, nil] explicit x translation
77
+ # @param dy [Numeric, nil] explicit y translation
78
+ # @return [Sevgi::Geometry::Equation::Linear::Diagonal]
31
79
  def shift(distance = nil, dx: nil, dy: nil)
32
80
  dx ||= 0.0
33
81
  dy ||= 0.0
@@ -40,6 +88,8 @@ module Sevgi
40
88
  Diagonal.new(slope:, intercept: intercept - (slope * dx) + dy)
41
89
  end
42
90
 
91
+ # Formats the equation for display.
92
+ # @return [String]
43
93
  def to_s
44
94
  strings = []
45
95
 
@@ -49,41 +99,134 @@ module Sevgi
49
99
  "Linear<y = #{strings.join(intercept.positive? ? " + " : " - ")}>"
50
100
  end
51
101
 
102
+ # Evaluates x for a y coordinate.
103
+ # @param y [Numeric] y coordinate
104
+ # @return [Float]
52
105
  def x(y) = (y - intercept) / slope
53
106
 
107
+ # Evaluates y for an x coordinate.
108
+ # @param x [Numeric] x coordinate
109
+ # @return [Float]
54
110
  def y(x) = (slope * x) + intercept
55
111
 
56
112
  alias == eql?
57
113
  end
58
114
 
115
+ # Horizontal linear equation in `y = c` form.
59
116
  class Horizontal < Diagonal
117
+ # Creates a horizontal equation.
118
+ # @param c [Numeric] y coordinate
119
+ # @return [void]
120
+ # @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
60
121
  def initialize(c) = super(slope: 0.0, intercept: c)
122
+
123
+ # Returns an equation rounded to precision.
124
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
125
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
126
+ def approx(precision = nil) = self.class.new(F.approx(intercept, precision))
127
+
128
+ # Returns a parallel horizontal equation shifted by offsets.
129
+ #
130
+ # A positive signed distance shifts upward in screen coordinates.
131
+ # @param distance [Numeric, nil] signed perpendicular offset
132
+ # @param dx [Numeric, nil] accepted for signature compatibility and ignored
133
+ # @param dy [Numeric, nil] explicit y translation
134
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
135
+ def shift(distance = nil, dx: nil, dy: nil)
136
+ _dx = dx
137
+
138
+ self.class.new(intercept + (dy || 0.0) - (distance || 0.0))
139
+ end
140
+
141
+ # Formats the equation for display.
142
+ # @return [String]
143
+ def to_s = "Linear<y = #{F.approx(intercept)}>"
61
144
  end
62
145
 
146
+ # Vertical linear equation in `x = c` form.
63
147
  class Vertical < Linear
148
+ # Creates a vertical equation.
149
+ # @param c [Numeric] x coordinate
150
+ # @return [void]
151
+ # @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
64
152
  def initialize(c)
65
153
  super()
66
154
 
67
- @x = c
155
+ @x = Real[:x, c]
68
156
  end
69
157
 
70
- def left?(point) = F.lt?(point.x, x(point.y))
158
+ # Returns an equation rounded to precision.
159
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
160
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
161
+ def approx(precision = nil) = self.class.new(F.approx(x, precision))
71
162
 
72
- def on?(point) = F.eq?(point.x, x(point.y))
163
+ # Reports strict equation equality.
164
+ # @param other [Object] object to compare
165
+ # @return [Boolean]
166
+ def eql?(other) = self.class == other.class && x == other.x
73
167
 
74
- def right?(point) = F.gt?(point.x, x(point.y))
168
+ # Returns a hash compatible with strict equality.
169
+ # @return [Integer]
170
+ def hash = [self.class, x].hash
75
171
 
172
+ # Reports whether a point is on the left side of the line.
173
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
174
+ # @return [Boolean]
175
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
176
+ def left?(point)
177
+ point = Tuple[Point, point]
178
+
179
+ F.lt?(point.x, x(point.y))
180
+ end
181
+
182
+ # Reports whether a point is on the line.
183
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
184
+ # @return [Boolean]
185
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
186
+ def on?(point)
187
+ point = Tuple[Point, point]
188
+
189
+ F.eq?(point.x, x(point.y))
190
+ end
191
+
192
+ # Reports whether a point is on the right side of the line.
193
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
194
+ # @return [Boolean]
195
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
196
+ def right?(point)
197
+ point = Tuple[Point, point]
198
+
199
+ F.gt?(point.x, x(point.y))
200
+ end
201
+
202
+ # Returns a parallel vertical equation shifted by offsets.
203
+ #
204
+ # A positive signed distance shifts right in screen coordinates.
205
+ # @param distance [Numeric, nil] signed perpendicular offset
206
+ # @param dx [Numeric, nil] explicit x translation
207
+ # @param dy [Numeric, nil] accepted for signature compatibility and ignored
208
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
76
209
  def shift(distance = nil, dx: nil, dy: nil)
77
210
  _dy = dy
78
211
 
79
212
  self.class.new(x + (distance || 0.0) + (dx || 0.0))
80
213
  end
81
214
 
215
+ # Formats the equation for display.
216
+ # @return [String]
82
217
  def to_s = "Linear<x = #{F.approx(x)}>"
83
218
 
219
+ # Evaluates x for a y coordinate.
220
+ # @param _ [Numeric, nil] ignored y coordinate
221
+ # @return [Float]
84
222
  def x(_ = nil) = @x
85
223
 
224
+ # Evaluates y for an x coordinate.
225
+ # @param _ [Numeric, nil] ignored x coordinate
226
+ # @return [Float] positive infinity because a vertical line has no single y
86
227
  def y(_ = nil) = ::Float::INFINITY
228
+
229
+ alias == eql?
87
230
  end
88
231
  end
89
232
  end
@@ -3,14 +3,23 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  class Equation
6
+ # Base class for quadratic equations.
6
7
  class Quadratic < Equation
7
8
  end
8
9
  end
9
10
 
11
+ # Reserved circle element support.
12
+ # @api private
10
13
  class Circle
14
+ # Returns the circle equation.
15
+ # @abstract Circle equation support is not implemented yet.
16
+ # @return [Sevgi::Geometry::Equation::Quadratic]
17
+ # @raise [Sevgi::PanicError] until circle equation support is implemented
11
18
  def equation
12
- raise NotImplementedError
19
+ PanicError.("#{self.class}#equation must be implemented")
13
20
  end
14
21
  end
22
+
23
+ private_constant :Circle
15
24
  end
16
25
  end
@@ -2,13 +2,32 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Base class for geometric equations that can intersect with each other.
5
6
  class Equation
7
+ # Builds a non-axis-aligned linear equation.
8
+ # @param slope [Numeric] line slope
9
+ # @param intercept [Numeric] y-intercept
10
+ # @return [Sevgi::Geometry::Equation::Linear::Diagonal]
11
+ # @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
6
12
  def self.diagonal(slope:, intercept:) = Linear::Diagonal.new(slope:, intercept:)
7
13
 
14
+ # Builds a horizontal linear equation.
15
+ # @param const [Numeric] y coordinate
16
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
17
+ # @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
8
18
  def self.horizontal(const) = Linear::Horizontal.new(const)
9
19
 
20
+ # Builds a vertical linear equation.
21
+ # @param const [Numeric] x coordinate
22
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
23
+ # @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
10
24
  def self.vertical(const) = Linear::Vertical.new(const)
11
25
 
26
+ # Intersects this equation with another equation.
27
+ # @param other [Sevgi::Geometry::Equation] equation to intersect with
28
+ # @return [Array<Sevgi::Geometry::Point>] intersection points
29
+ # @raise [Sevgi::Geometry::Error] when other is not an equation
30
+ # @raise [Sevgi::PanicError] when the equation combination is not implemented
12
31
  def intersect(other)
13
32
  Error.("Must be an equation: #{other}") unless other.is_a?(Equation)
14
33
 
@@ -20,41 +39,61 @@ module Sevgi
20
39
  in [Quadratic, Quadratic]
21
40
  quadratic_vs_quadratic(other)
22
41
  else
23
- Error.("TODO")
42
+ PanicError.("Intersection not implemented: #{self.class} / #{other.class}")
24
43
  end
25
44
 
26
45
  Array(points)
27
46
  end
28
47
 
29
- def y(_x, ...) = raise NoMethodError, "#{self.class}#y must be implemented"
48
+ # Evaluates y for an x coordinate.
49
+ # @abstract Subclasses implement equation-specific mapping.
50
+ # @param _x [Numeric] x coordinate
51
+ # @return [Float]
52
+ # @raise [Sevgi::PanicError] when a subclass does not implement y
53
+ def y(_x, ...) = PanicError.("#{self.class}#y must be implemented")
30
54
 
31
55
  private
32
56
 
33
57
  def linear_vs_linear(other)
34
58
  case [self, other]
35
59
  in [Linear::Diagonal, Linear::Diagonal]
36
- y = self.y(x = (other.intercept - intercept) / (slope - other.slope))
60
+ diagonal_vs_diagonal(self, other)
37
61
  in [Linear::Diagonal, Linear::Vertical]
38
- y = self.y(x = other.x)
62
+ diagonal_vs_vertical(self, other)
39
63
  in [Linear::Vertical, Linear::Diagonal]
40
- y = other.y(x = self.x())
64
+ diagonal_vs_vertical(other, self)
41
65
  in [Linear::Vertical, Linear::Vertical]
42
- x, y = ::Float::INFINITY, ::Float::INFINITY
66
+ nil
43
67
  end
68
+ end
69
+
70
+ def diagonal_vs_diagonal(left, right)
71
+ return nil if F.eq?(left.slope, right.slope)
72
+
73
+ x = (right.intercept - left.intercept) / (left.slope - right.slope)
74
+
75
+ Point[x, left.y(x)]
76
+ end
77
+
78
+ def diagonal_vs_vertical(diagonal, vertical)
79
+ x = vertical.x
44
80
 
45
- Point[x, y]
81
+ Point[x, diagonal.y(x)]
46
82
  end
47
83
 
48
84
  def linear_vs_quadratic(...)
49
- raise NotImplementedError
85
+ PanicError.("Linear/quadratic intersection must be implemented")
50
86
  end
51
87
 
52
88
  def quadratic_vs_quadratic(...)
53
- raise NotImplementedError
89
+ PanicError.("Quadratic/quadratic intersection must be implemented")
54
90
  end
55
91
  end
56
92
 
57
93
  class Point
94
+ # Returns the linear equation passing through this point at an angle.
95
+ # @param angle [Numeric] clockwise angle in degrees
96
+ # @return [Sevgi::Geometry::Equation::Linear]
58
97
  def equation(angle)
59
98
  return Equation.horizontal(y) if F.zero?(angle % 180.0)
60
99
  return Equation.vertical(x) if F.zero?(angle % 90.0)
@@ -64,6 +103,8 @@ module Sevgi
64
103
  end
65
104
 
66
105
  class Line
106
+ # Returns the linear equation containing this line.
107
+ # @return [Sevgi::Geometry::Equation::Linear]
67
108
  def equation = position.equation(angle)
68
109
  end
69
110
 
@@ -8,6 +8,7 @@ module Sevgi
8
8
  end
9
9
 
10
10
  module Geometry
11
+ # Base error for geometry input and operation failures.
11
12
  Error = Class.new(Error)
12
13
  end
13
14
  end