sevgi-geometry 0.73.2 → 0.93.1

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,43 @@
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 adjacent segments.
11
+ class Triangle < TriangleBase
12
+ # Builds a triangle from two adjacent segments.
13
+ # @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first segment
14
+ # @param segment_b [Sevgi::Geometry::Segment, Array<Numeric>] second segment
15
+ # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
16
+ # @return [Sevgi::Geometry::Triangle]
17
+ # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
18
+ def self.[](segment_a, segment_b, position: Origin)
19
+ a, b = Tuples[Segment, segment_a, segment_b]
20
+
21
+ new_by_segments(a, b, closing_segment(a, b), position:)
22
+ end
23
+
24
+ def self.closing_segment(a, b)
25
+ b_reverse_angle = b.angle - 180.0
26
+ angle_between = b_reverse_angle - a.angle
27
+ length = closing_length(a, b, angle_between)
28
+
29
+ Segment[length, closing_angle(a, b_reverse_angle, angle_between, length)]
30
+ end
31
+
32
+ def self.closing_angle(a, b_reverse_angle, angle_between, length)
33
+ b_reverse_angle + F.asin(a.length * F.sin(angle_between) / length)
34
+ end
35
+
36
+ def self.closing_length(a, b, angle_between)
37
+ ::Math.sqrt((a.length ** 2) + (b.length ** 2) - (2 * a.length * b.length * F.cos(angle_between)))
38
+ end
39
+
40
+ private_class_method :closing_angle, :closing_length, :closing_segment
41
+ end
42
+ end
43
+ end
@@ -3,10 +3,20 @@
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]
10
20
  def initialize(slope:, intercept:)
11
21
  super()
12
22
 
@@ -14,20 +24,57 @@ module Sevgi
14
24
  @intercept = intercept.to_f
15
25
  end
16
26
 
27
+ # Returns an equation rounded to precision.
28
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
29
+ # @return [Sevgi::Geometry::Equation::Linear::Diagonal]
17
30
  def approx(precision = nil)
18
31
  self.class.new(slope: F.approx(slope, precision), intercept: F.approx(intercept, precision))
19
32
  end
20
33
 
34
+ # Reports strict equation equality.
35
+ # @param other [Object] object to compare
36
+ # @return [Boolean]
21
37
  def eql?(other) = self.class == other.class && [slope, intercept] == [other.slope, other.intercept]
22
38
 
39
+ # Returns a hash compatible with strict equality.
40
+ # @return [Integer]
23
41
  def hash = [self.class, slope, intercept].hash
24
42
 
25
- def left?(point) = F.gt?(point.y, y(point.x))
43
+ # Reports whether a point is on the left side of the line in screen coordinates.
44
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
45
+ # @return [Boolean]
46
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
47
+ def left?(point)
48
+ point = Tuple[Point, point]
26
49
 
27
- def on?(point) = F.eq?(point.y, y(point.x))
50
+ F.gt?(point.y, y(point.x))
51
+ end
52
+
53
+ # Reports whether a point is on the line.
54
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
55
+ # @return [Boolean]
56
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
57
+ def on?(point)
58
+ point = Tuple[Point, point]
59
+
60
+ F.eq?(point.y, y(point.x))
61
+ end
62
+
63
+ # Reports whether a point is on the right side of the line in screen coordinates.
64
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
65
+ # @return [Boolean]
66
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
67
+ def right?(point)
68
+ point = Tuple[Point, point]
28
69
 
29
- def right?(point) = F.lt?(point.y, y(point.x))
70
+ F.lt?(point.y, y(point.x))
71
+ end
30
72
 
73
+ # Returns a parallel equation shifted by a signed perpendicular offset.
74
+ # @param distance [Numeric, nil] signed perpendicular offset
75
+ # @param dx [Numeric, nil] explicit x translation
76
+ # @param dy [Numeric, nil] explicit y translation
77
+ # @return [Sevgi::Geometry::Equation::Linear::Diagonal]
31
78
  def shift(distance = nil, dx: nil, dy: nil)
32
79
  dx ||= 0.0
33
80
  dy ||= 0.0
@@ -40,6 +87,8 @@ module Sevgi
40
87
  Diagonal.new(slope:, intercept: intercept - (slope * dx) + dy)
41
88
  end
42
89
 
90
+ # Formats the equation for display.
91
+ # @return [String]
43
92
  def to_s
44
93
  strings = []
45
94
 
@@ -49,41 +98,132 @@ module Sevgi
49
98
  "Linear<y = #{strings.join(intercept.positive? ? " + " : " - ")}>"
50
99
  end
51
100
 
101
+ # Evaluates x for a y coordinate.
102
+ # @param y [Numeric] y coordinate
103
+ # @return [Float]
52
104
  def x(y) = (y - intercept) / slope
53
105
 
106
+ # Evaluates y for an x coordinate.
107
+ # @param x [Numeric] x coordinate
108
+ # @return [Float]
54
109
  def y(x) = (slope * x) + intercept
55
110
 
56
111
  alias == eql?
57
112
  end
58
113
 
114
+ # Horizontal linear equation in `y = c` form.
59
115
  class Horizontal < Diagonal
116
+ # Creates a horizontal equation.
117
+ # @param c [Numeric] y coordinate
118
+ # @return [void]
60
119
  def initialize(c) = super(slope: 0.0, intercept: c)
120
+
121
+ # Returns an equation rounded to precision.
122
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
123
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
124
+ def approx(precision = nil) = self.class.new(F.approx(intercept, precision))
125
+
126
+ # Returns a parallel horizontal equation shifted by offsets.
127
+ #
128
+ # A positive signed distance shifts upward in screen coordinates.
129
+ # @param distance [Numeric, nil] signed perpendicular offset
130
+ # @param dx [Numeric, nil] accepted for signature compatibility and ignored
131
+ # @param dy [Numeric, nil] explicit y translation
132
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
133
+ def shift(distance = nil, dx: nil, dy: nil)
134
+ _dx = dx
135
+
136
+ self.class.new(intercept + (dy || 0.0) - (distance || 0.0))
137
+ end
138
+
139
+ # Formats the equation for display.
140
+ # @return [String]
141
+ def to_s = "Linear<y = #{F.approx(intercept)}>"
61
142
  end
62
143
 
144
+ # Vertical linear equation in `x = c` form.
63
145
  class Vertical < Linear
146
+ # Creates a vertical equation.
147
+ # @param c [Numeric] x coordinate
148
+ # @return [void]
64
149
  def initialize(c)
65
150
  super()
66
151
 
67
- @x = c
152
+ @x = c.to_f
68
153
  end
69
154
 
70
- def left?(point) = F.lt?(point.x, x(point.y))
155
+ # Returns an equation rounded to precision.
156
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
157
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
158
+ def approx(precision = nil) = self.class.new(F.approx(x, precision))
71
159
 
72
- def on?(point) = F.eq?(point.x, x(point.y))
160
+ # Reports strict equation equality.
161
+ # @param other [Object] object to compare
162
+ # @return [Boolean]
163
+ def eql?(other) = self.class == other.class && x == other.x
73
164
 
74
- def right?(point) = F.gt?(point.x, x(point.y))
165
+ # Returns a hash compatible with strict equality.
166
+ # @return [Integer]
167
+ def hash = [self.class, x].hash
75
168
 
169
+ # Reports whether a point is on the left side of the line.
170
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
171
+ # @return [Boolean]
172
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
173
+ def left?(point)
174
+ point = Tuple[Point, point]
175
+
176
+ F.lt?(point.x, x(point.y))
177
+ end
178
+
179
+ # Reports whether a point is on the line.
180
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
181
+ # @return [Boolean]
182
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
183
+ def on?(point)
184
+ point = Tuple[Point, point]
185
+
186
+ F.eq?(point.x, x(point.y))
187
+ end
188
+
189
+ # Reports whether a point is on the right side of the line.
190
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
191
+ # @return [Boolean]
192
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
193
+ def right?(point)
194
+ point = Tuple[Point, point]
195
+
196
+ F.gt?(point.x, x(point.y))
197
+ end
198
+
199
+ # Returns a parallel vertical equation shifted by offsets.
200
+ #
201
+ # A positive signed distance shifts right in screen coordinates.
202
+ # @param distance [Numeric, nil] signed perpendicular offset
203
+ # @param dx [Numeric, nil] explicit x translation
204
+ # @param dy [Numeric, nil] accepted for signature compatibility and ignored
205
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
76
206
  def shift(distance = nil, dx: nil, dy: nil)
77
207
  _dy = dy
78
208
 
79
209
  self.class.new(x + (distance || 0.0) + (dx || 0.0))
80
210
  end
81
211
 
212
+ # Formats the equation for display.
213
+ # @return [String]
82
214
  def to_s = "Linear<x = #{F.approx(x)}>"
83
215
 
216
+ # Evaluates x for a y coordinate.
217
+ # @param _ [Numeric, nil] ignored y coordinate
218
+ # @return [Float]
84
219
  def x(_ = nil) = @x
85
220
 
221
+ # Evaluates y for an x coordinate.
222
+ # @param _ [Numeric, nil] ignored x coordinate
223
+ # @return [Float] positive infinity because a vertical line has no single y
86
224
  def y(_ = nil) = ::Float::INFINITY
225
+
226
+ alias == eql?
87
227
  end
88
228
  end
89
229
  end
@@ -3,13 +3,19 @@
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
+ # Circle equation support is not implemented yet.
10
12
  class Circle
13
+ # Returns the circle equation.
14
+ # @abstract Circle equation support is not implemented yet.
15
+ # @return [Sevgi::Geometry::Equation::Quadratic]
16
+ # @raise [Sevgi::PanicError] until circle equation support is implemented
11
17
  def equation
12
- raise NotImplementedError
18
+ PanicError.("#{self.class}#equation must be implemented")
13
19
  end
14
20
  end
15
21
  end
@@ -2,13 +2,29 @@
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]
6
11
  def self.diagonal(slope:, intercept:) = Linear::Diagonal.new(slope:, intercept:)
7
12
 
13
+ # Builds a horizontal linear equation.
14
+ # @param const [Numeric] y coordinate
15
+ # @return [Sevgi::Geometry::Equation::Linear::Horizontal]
8
16
  def self.horizontal(const) = Linear::Horizontal.new(const)
9
17
 
18
+ # Builds a vertical linear equation.
19
+ # @param const [Numeric] x coordinate
20
+ # @return [Sevgi::Geometry::Equation::Linear::Vertical]
10
21
  def self.vertical(const) = Linear::Vertical.new(const)
11
22
 
23
+ # Intersects this equation with another equation.
24
+ # @param other [Sevgi::Geometry::Equation] equation to intersect with
25
+ # @return [Array<Sevgi::Geometry::Point>] intersection points
26
+ # @raise [Sevgi::Geometry::Error] when other is not an equation
27
+ # @raise [Sevgi::PanicError] when the equation combination is not implemented
12
28
  def intersect(other)
13
29
  Error.("Must be an equation: #{other}") unless other.is_a?(Equation)
14
30
 
@@ -20,41 +36,61 @@ module Sevgi
20
36
  in [Quadratic, Quadratic]
21
37
  quadratic_vs_quadratic(other)
22
38
  else
23
- Error.("TODO")
39
+ PanicError.("Intersection not implemented: #{self.class} / #{other.class}")
24
40
  end
25
41
 
26
42
  Array(points)
27
43
  end
28
44
 
29
- def y(_x, ...) = raise NoMethodError, "#{self.class}#y must be implemented"
45
+ # Evaluates y for an x coordinate.
46
+ # @abstract Subclasses implement equation-specific mapping.
47
+ # @param _x [Numeric] x coordinate
48
+ # @return [Float]
49
+ # @raise [Sevgi::PanicError] when a subclass does not implement y
50
+ def y(_x, ...) = PanicError.("#{self.class}#y must be implemented")
30
51
 
31
52
  private
32
53
 
33
54
  def linear_vs_linear(other)
34
55
  case [self, other]
35
56
  in [Linear::Diagonal, Linear::Diagonal]
36
- y = self.y(x = (other.intercept - intercept) / (slope - other.slope))
57
+ diagonal_vs_diagonal(self, other)
37
58
  in [Linear::Diagonal, Linear::Vertical]
38
- y = self.y(x = other.x)
59
+ diagonal_vs_vertical(self, other)
39
60
  in [Linear::Vertical, Linear::Diagonal]
40
- y = other.y(x = self.x())
61
+ diagonal_vs_vertical(other, self)
41
62
  in [Linear::Vertical, Linear::Vertical]
42
- x, y = ::Float::INFINITY, ::Float::INFINITY
63
+ nil
43
64
  end
65
+ end
66
+
67
+ def diagonal_vs_diagonal(left, right)
68
+ return nil if F.eq?(left.slope, right.slope)
69
+
70
+ x = (right.intercept - left.intercept) / (left.slope - right.slope)
71
+
72
+ Point[x, left.y(x)]
73
+ end
74
+
75
+ def diagonal_vs_vertical(diagonal, vertical)
76
+ x = vertical.x
44
77
 
45
- Point[x, y]
78
+ Point[x, diagonal.y(x)]
46
79
  end
47
80
 
48
81
  def linear_vs_quadratic(...)
49
- raise NotImplementedError
82
+ PanicError.("Linear/quadratic intersection must be implemented")
50
83
  end
51
84
 
52
85
  def quadratic_vs_quadratic(...)
53
- raise NotImplementedError
86
+ PanicError.("Quadratic/quadratic intersection must be implemented")
54
87
  end
55
88
  end
56
89
 
57
90
  class Point
91
+ # Returns the linear equation passing through this point at an angle.
92
+ # @param angle [Numeric] clockwise angle in degrees
93
+ # @return [Sevgi::Geometry::Equation::Linear]
58
94
  def equation(angle)
59
95
  return Equation.horizontal(y) if F.zero?(angle % 180.0)
60
96
  return Equation.vertical(x) if F.zero?(angle % 90.0)
@@ -64,6 +100,8 @@ module Sevgi
64
100
  end
65
101
 
66
102
  class Line
103
+ # Returns the linear equation containing this line.
104
+ # @return [Sevgi::Geometry::Equation::Linear]
67
105
  def equation = position.equation(angle)
68
106
  end
69
107
 
@@ -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
@@ -2,7 +2,14 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Coerces array-like geometry inputs into typed tuple objects.
6
+ # @api private
5
7
  module Tuple
8
+ # Coerces an argument into the requested tuple class.
9
+ # @param klass [Class] tuple class such as {Point} or {Segment}
10
+ # @param arg [Array<Numeric>, Object] tuple instance or two-element numeric array
11
+ # @return [Object] tuple instance
12
+ # @raise [Sevgi::Geometry::Error] when arg cannot be coerced
6
13
  def self.[](klass, arg)
7
14
  case arg
8
15
  when ::Array
@@ -20,7 +27,14 @@ module Sevgi
20
27
 
21
28
  private_constant :Tuple
22
29
 
30
+ # Coerces multiple array-like geometry inputs into typed tuple objects.
31
+ # @api private
23
32
  module Tuples
33
+ # Coerces arguments into the requested tuple class.
34
+ # @param klass [Class] tuple class such as {Point} or {Segment}
35
+ # @param args [Array<Array<Numeric>, Object>] tuple instances or two-element numeric arrays
36
+ # @return [Array<Object>] tuple instances
37
+ # @raise [Sevgi::Geometry::Error] when any argument cannot be coerced
24
38
  def self.[](klass, *args) = args.map { Tuple[klass, it] }
25
39
  end
26
40
 
@@ -3,33 +3,59 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  module Operation
6
+ # Alignment operation implementation.
6
7
  module Align
8
+ extend self
9
+
10
+ # Returns an element translated to align with another element.
11
+ # @param element [Sevgi::Geometry::Element] element to move
12
+ # @param other [Sevgi::Geometry::Element] reference element
13
+ # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
14
+ # @return [Sevgi::Geometry::Element] translated element
15
+ # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when other is not a geometry element
16
+ # @raise [Sevgi::ArgumentError] when alignment is unknown
7
17
  def align(element, other, alignment = :center)
8
- alignment(element, other, alignment).apply(element)
18
+ offset = alignment(element, other, alignment)
19
+
20
+ element.translate(offset.x, offset.y)
9
21
  end
10
22
 
23
+ # Returns the offset needed to align one element with another.
24
+ # @param element [Sevgi::Geometry::Element] element to move
25
+ # @param other [Sevgi::Geometry::Element] reference element
26
+ # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
27
+ # @return [Sevgi::Geometry::Point] translation offset
28
+ # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when other is not a geometry element
29
+ # @raise [Sevgi::ArgumentError] when alignment is unknown
11
30
  def alignment(element, other, alignment = :center)
31
+ OperationInapplicableError.("Not a Geometric Element: #{other}") unless other.is_a?(Element)
32
+
12
33
  this, that = element.box, other.box
13
34
 
14
35
  case alignment
15
36
  when :center
16
- Translation[(this.width - that.width) / 2.0, (this.height - that.height) / 2.0]
37
+ Point[
38
+ that.position.x + ((that.width - this.width) / 2.0) - this.position.x,
39
+ that.position.y + ((that.height - this.height) / 2.0) - this.position.y
40
+ ]
17
41
  when :left
18
- Translation[this.ne.x - that.ne.x, 0]
42
+ Point[that.position.x - this.position.x, 0]
19
43
  when :right
20
- Translation[this.sw.x - that.sw.x, 0]
44
+ Point[(that.position.x + that.width) - (this.position.x + this.width), 0]
21
45
  when :top
22
- Translation[0, this.ne.y - that.ne.y]
46
+ Point[0, that.position.y - this.position.y]
23
47
  when :bottom
24
- Translation[0, this.sw.y - that.sw.y]
48
+ Point[0, (that.position.y + that.height) - (this.position.y + this.height)]
25
49
  else
26
50
  ArgumentError.("No such type of alignment: #{alignment}")
27
51
  end
28
52
  end
29
53
 
30
- def applicable?(element)
31
- Translation.applicable?(element)
32
- end
54
+ # Reports whether the alignment handler can operate on an element.
55
+ # @api private
56
+ # @param element [Object] candidate element
57
+ # @return [Boolean]
58
+ def applicable?(element) = element.respond_to?(:translate)
33
59
  end
34
60
 
35
61
  register(Align, :align, :alignment)
@@ -3,13 +3,27 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  module Operation
6
+ # Sweep operation implementation.
6
7
  module Sweep
7
8
  extend self
8
9
 
10
+ # Default maximum number of sweep iterations.
9
11
  LIMIT = 1_000
10
12
 
11
- def sweep(element, initial:, direction:, step:, limit: LIMIT, &block)
12
- equation = Tuple[Point, initial].equation(direction)
13
+ # Sweeps parallel lines across an element in both directions.
14
+ # @param element [Sevgi::Geometry::Element] element to intersect
15
+ # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
16
+ # @param angle [Numeric] clockwise sweep line angle in degrees
17
+ # @param step [Numeric] signed distance between sweep lines
18
+ # @param limit [Integer] maximum iterations per direction
19
+ # @yield [lines] optional hook receiving the generated lines
20
+ # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
21
+ # @yieldreturn [void]
22
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
23
+ # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
24
+ # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
25
+ def sweep(element, initial:, angle:, step:, limit: LIMIT, &block)
26
+ equation = Tuple[Point, initial].equation(angle)
13
27
 
14
28
  [
15
29
  *unisweep(element, equation.shift(-step), -step, limit:).reverse,
@@ -19,16 +33,35 @@ module Sevgi
19
33
  end
20
34
  end
21
35
 
22
- def sweep!(element, initial:, direction:, step:, limit: LIMIT, &block)
23
- sweep(element, initial:, direction:, step:, limit:) do |lines|
36
+ # Sweeps parallel lines and requires at least one result.
37
+ # @param element [Sevgi::Geometry::Element] element to intersect
38
+ # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
39
+ # @param angle [Numeric] clockwise sweep line angle in degrees
40
+ # @param step [Numeric] signed distance between sweep lines
41
+ # @param limit [Integer] maximum iterations per direction
42
+ # @yield [lines] optional hook receiving the generated lines
43
+ # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
44
+ # @yieldreturn [void]
45
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
46
+ # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
47
+ # @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
48
+ def sweep!(element, initial:, angle:, step:, limit: LIMIT, &block)
49
+ sweep(element, initial:, angle:, step:, limit:) do |lines|
24
50
  if lines.empty?
25
- OperationError.("No lines found [initial: #{initial}, direction: #{direction} step: #{step}]")
51
+ OperationError.("No lines found [initial: #{initial}, angle: #{angle} step: #{step}]")
26
52
  end
27
53
 
28
54
  yield(lines) if block
29
55
  end
30
56
  end
31
57
 
58
+ # Sweeps parallel lines in one signed direction from an equation.
59
+ # @param element [Sevgi::Geometry::Element] element to intersect
60
+ # @param equation [Sevgi::Geometry::Equation] initial sweep equation
61
+ # @param step [Numeric] signed distance between sweep lines
62
+ # @param limit [Integer] maximum iterations
63
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
64
+ # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
32
65
  def unisweep(element, equation, step, limit: LIMIT)
33
66
  lines = []
34
67
 
@@ -46,6 +79,10 @@ module Sevgi
46
79
  OperationError.("Loop limit reached: #{limit}")
47
80
  end
48
81
 
82
+ # Reports whether the sweep handler can operate on an element.
83
+ # @api private
84
+ # @param element [Object] candidate element
85
+ # @return [Boolean]
49
86
  def applicable?(element)
50
87
  element.respond_to?(:intersection)
51
88
  end
@@ -2,12 +2,21 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Dispatches geometry operations to operation handler modules.
5
6
  module Operation
6
7
  extend self
7
8
 
9
+ # Raised when an operation starts but cannot complete.
8
10
  OperationError = Class.new(Error)
11
+
12
+ # Raised when an operation does not apply to the target element.
9
13
  OperationInapplicableError = Class.new(Error)
10
14
 
15
+ # Registers one or more public operation methods.
16
+ # @api private
17
+ # @param handler [Module] operation handler module
18
+ # @param operations [Array<Symbol>] operation method names
19
+ # @return [Array<Symbol>] registered operation names
11
20
  def register(handler, *operations) = operations.each { |operation| def_operation(operation, handler) }
12
21
 
13
22
  private