sevgi-geometry 0.94.0 → 0.98.2
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/CHANGELOG.md +275 -2
- data/LICENSE +672 -3
- data/README.md +7 -7
- data/lib/sevgi/geometry/element.rb +238 -121
- data/lib/sevgi/geometry/elements/line.rb +60 -17
- data/lib/sevgi/geometry/elements/parallelogram.rb +110 -29
- data/lib/sevgi/geometry/elements/polygon.rb +41 -1
- data/lib/sevgi/geometry/elements/polyline.rb +36 -1
- data/lib/sevgi/geometry/elements/rect.rb +159 -29
- data/lib/sevgi/geometry/elements/triangle.rb +60 -11
- data/lib/sevgi/geometry/equation/linear.rb +138 -74
- data/lib/sevgi/geometry/equation/quadratic.rb +4 -1
- data/lib/sevgi/geometry/equation.rb +36 -13
- data/lib/sevgi/geometry/internal.rb +19 -7
- data/lib/sevgi/geometry/operation/align.rb +1 -0
- data/lib/sevgi/geometry/operation/sweep.rb +24 -6
- data/lib/sevgi/geometry/operation.rb +28 -14
- data/lib/sevgi/geometry/point.rb +115 -21
- data/lib/sevgi/geometry/segment.rb +76 -21
- data/lib/sevgi/geometry/version.rb +1 -1
- data/lib/sevgi/geometry.rb +25 -2
- metadata +6 -6
|
@@ -7,7 +7,48 @@ module Sevgi
|
|
|
7
7
|
TriangleBase = Element.lined(3)
|
|
8
8
|
private_constant :TriangleBase
|
|
9
9
|
|
|
10
|
-
# Closed three-sided element built from
|
|
10
|
+
# Closed three-sided element built from non-collinear segments or points. Every construction path rejects
|
|
11
|
+
# degenerate triangles; affine operations retain Triangle when the transformed points remain non-degenerate.
|
|
12
|
+
# @!method self.call(*points)
|
|
13
|
+
# Builds a triangle from three boundary points.
|
|
14
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] three boundary points
|
|
15
|
+
# @return [Sevgi::Geometry::Triangle]
|
|
16
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or form a degenerate triangle
|
|
17
|
+
# @!method self.from_segments(segment_a, segment_b, position: Origin)
|
|
18
|
+
# Builds a triangle from two adjacent segments and derives the closing side.
|
|
19
|
+
# @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first adjacent segment
|
|
20
|
+
# @param segment_b [Sevgi::Geometry::Segment, Array<Numeric>] second adjacent segment
|
|
21
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
22
|
+
# @return [Sevgi::Geometry::Triangle]
|
|
23
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or form a degenerate triangle
|
|
24
|
+
# @!method self.from_points(*points)
|
|
25
|
+
# Builds a triangle from three boundary points.
|
|
26
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] three boundary points
|
|
27
|
+
# @return [Sevgi::Geometry::Triangle]
|
|
28
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or form a degenerate triangle
|
|
29
|
+
# @!attribute [r] A
|
|
30
|
+
# @return [Sevgi::Geometry::Point] first vertex
|
|
31
|
+
# @!attribute [r] B
|
|
32
|
+
# @return [Sevgi::Geometry::Point] second vertex
|
|
33
|
+
# @!attribute [r] C
|
|
34
|
+
# @return [Sevgi::Geometry::Point] third vertex
|
|
35
|
+
# @!attribute [r] AB
|
|
36
|
+
# @return [Sevgi::Geometry::Line] side from A to B
|
|
37
|
+
# @!attribute [r] BC
|
|
38
|
+
# @return [Sevgi::Geometry::Line] side from B to C
|
|
39
|
+
# @!attribute [r] CA
|
|
40
|
+
# @return [Sevgi::Geometry::Line] side from C to A
|
|
41
|
+
# @!method perimeter
|
|
42
|
+
# Returns the closed path perimeter.
|
|
43
|
+
# @return [Float]
|
|
44
|
+
# @example Pair mathematical notation with English conveniences
|
|
45
|
+
# Sevgi::Geometry::Triangle[[2, 0], [2, 90]] == Sevgi::Geometry::Triangle.from_segments([2, 0], [2, 90])
|
|
46
|
+
# Sevgi::Geometry::Triangle.([0, 0], [2, 0], [2, 2]) == Sevgi::Geometry::Triangle.from_points([0, 0], [2, 0], [2, 2])
|
|
47
|
+
# @example Use named vertices and sides
|
|
48
|
+
# triangle = Sevgi::Geometry::Triangle.([0, 0], [3, 0], [3, 4])
|
|
49
|
+
# triangle.C.deconstruct # => [3.0, 4.0]
|
|
50
|
+
# triangle.AB.length # => 3.0
|
|
51
|
+
# triangle.perimeter # => 12.0
|
|
11
52
|
class Triangle < TriangleBase
|
|
12
53
|
# Builds a triangle from two adjacent segments.
|
|
13
54
|
#
|
|
@@ -27,21 +68,29 @@ module Sevgi
|
|
|
27
68
|
new_by_segments(a, b, closing_segment(a, b), position:)
|
|
28
69
|
end
|
|
29
70
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
71
|
+
class << self
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def closing_segment(a, b)
|
|
75
|
+
Segment.(b.ending(a.ending(Origin)), Origin)
|
|
76
|
+
end
|
|
33
77
|
|
|
34
|
-
|
|
78
|
+
def cross(a, b) = (a.x * b.y) - (a.y * b.x)
|
|
35
79
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
80
|
+
def validate!(a, b)
|
|
81
|
+
if F.zero?(a.length) ||
|
|
82
|
+
F.zero?(b.length) ||
|
|
83
|
+
F.zero?(cross(a, b))
|
|
84
|
+
Error.("Triangle segments must form a non-degenerate triangle")
|
|
85
|
+
end
|
|
41
86
|
end
|
|
42
87
|
end
|
|
43
88
|
|
|
44
|
-
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def validate_geometry!
|
|
92
|
+
self.class.send(:validate!, segments[0], segments[1])
|
|
93
|
+
end
|
|
45
94
|
end
|
|
46
95
|
end
|
|
47
96
|
end
|
|
@@ -3,26 +3,80 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
5
|
class Equation
|
|
6
|
-
# Base class for linear equations.
|
|
6
|
+
# Base class for unoriented linear equations.
|
|
7
|
+
#
|
|
8
|
+
# An equation has no ordered endpoints, so it does not define intrinsic left and right sides. Signed shifts use a
|
|
9
|
+
# canonical direction only to select a normal: increasing x for non-vertical equations and increasing y for
|
|
10
|
+
# vertical equations. Positive distance moves to screen-left of that canonical direction.
|
|
7
11
|
class Linear < Equation
|
|
12
|
+
def shift_values(distance, dx, dy)
|
|
13
|
+
[[:distance, distance], [:dx, dx], [:dy, dy]].map do |field, value|
|
|
14
|
+
value.nil? ? 0.0 : Real[field, value]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private :shift_values
|
|
19
|
+
|
|
20
|
+
# Shared behavior for equations that map x coordinates to y coordinates.
|
|
21
|
+
# @api private
|
|
22
|
+
module Nonvertical
|
|
23
|
+
# Reports strict equality for two nonvertical equations.
|
|
24
|
+
# @param equation [Sevgi::Geometry::Equation::Linear] equation to compare
|
|
25
|
+
# @param other [Object] comparison target
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def self.equal?(equation, other)
|
|
28
|
+
equation.class == other.class && [equation.slope, equation.intercept] == [other.slope, other.intercept]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns a strict-equality hash for a nonvertical equation.
|
|
32
|
+
# @param equation [Sevgi::Geometry::Equation::Linear] equation to hash
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def self.hash(equation) = [equation.class, equation.slope, equation.intercept].hash
|
|
35
|
+
|
|
36
|
+
# Reports whether a point is on a nonvertical equation.
|
|
37
|
+
# @param equation [Sevgi::Geometry::Equation::Linear] equation to test
|
|
38
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
41
|
+
def self.on?(equation, point)
|
|
42
|
+
point = Tuple[Point, point]
|
|
43
|
+
|
|
44
|
+
F.eq?(point.y, y(equation, point.x))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Evaluates a nonvertical equation at an x coordinate.
|
|
48
|
+
# @param equation [Sevgi::Geometry::Equation::Linear] equation to evaluate
|
|
49
|
+
# @param x [Numeric] x coordinate
|
|
50
|
+
# @return [Float]
|
|
51
|
+
# @raise [Sevgi::Geometry::Error] when x is not a finite real number
|
|
52
|
+
def self.y(equation, x) = (equation.slope * Real[:x, x]) + equation.intercept
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_constant :Nonvertical
|
|
56
|
+
|
|
8
57
|
# Non-axis-aligned linear equation in `y = slope * x + intercept` form.
|
|
9
58
|
class Diagonal < Linear
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
attr_reader :slope
|
|
59
|
+
public_class_method :new
|
|
60
|
+
|
|
61
|
+
# Returns the line slope.
|
|
62
|
+
# @return [Float]
|
|
63
|
+
attr_reader :slope
|
|
64
|
+
|
|
65
|
+
# Returns the y-intercept.
|
|
66
|
+
# @return [Float]
|
|
67
|
+
attr_reader :intercept
|
|
15
68
|
|
|
16
69
|
# Creates a diagonal linear equation.
|
|
17
70
|
# @param slope [Numeric] line slope
|
|
18
71
|
# @param intercept [Numeric] y-intercept
|
|
19
72
|
# @return [void]
|
|
20
|
-
# @raise [Sevgi::Geometry::Error] when a coefficient is not
|
|
73
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not finite Numeric or slope is zero
|
|
21
74
|
def initialize(slope:, intercept:)
|
|
22
75
|
super()
|
|
23
76
|
|
|
24
77
|
@slope = Real[:slope, slope]
|
|
25
78
|
@intercept = Real[:intercept, intercept]
|
|
79
|
+
Error.("A diagonal equation requires a non-zero slope") if @slope.zero?
|
|
26
80
|
end
|
|
27
81
|
|
|
28
82
|
# Returns an equation rounded to precision.
|
|
@@ -35,55 +89,30 @@ module Sevgi
|
|
|
35
89
|
# Reports strict equation equality.
|
|
36
90
|
# @param other [Object] object to compare
|
|
37
91
|
# @return [Boolean]
|
|
38
|
-
def eql?(other) = self
|
|
92
|
+
def eql?(other) = Nonvertical.equal?(self, other)
|
|
39
93
|
|
|
40
94
|
# Returns a hash compatible with strict equality.
|
|
41
95
|
# @return [Integer]
|
|
42
|
-
def hash =
|
|
43
|
-
|
|
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]
|
|
50
|
-
|
|
51
|
-
F.gt?(point.y, y(point.x))
|
|
52
|
-
end
|
|
96
|
+
def hash = Nonvertical.hash(self)
|
|
53
97
|
|
|
54
98
|
# Reports whether a point is on the line.
|
|
55
99
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
56
100
|
# @return [Boolean]
|
|
57
101
|
# @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]
|
|
70
|
-
|
|
71
|
-
F.lt?(point.y, y(point.x))
|
|
72
|
-
end
|
|
102
|
+
def on?(point) = Nonvertical.on?(self, point)
|
|
73
103
|
|
|
74
104
|
# Returns a parallel equation shifted by a signed perpendicular offset.
|
|
105
|
+
# Positive distance moves to screen-left of the equation's canonical increasing-x direction.
|
|
75
106
|
# @param distance [Numeric, nil] signed perpendicular offset
|
|
76
107
|
# @param dx [Numeric, nil] explicit x translation
|
|
77
108
|
# @param dy [Numeric, nil] explicit y translation
|
|
78
109
|
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
110
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
79
111
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
80
|
-
dx
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
dx += distance * F.sin(angle = F.atan(slope))
|
|
85
|
-
dy -= distance * F.cos(angle)
|
|
86
|
-
end
|
|
112
|
+
distance, dx, dy = shift_values(distance, dx, dy)
|
|
113
|
+
angle = F.atan(slope)
|
|
114
|
+
dx += distance * F.sin(angle)
|
|
115
|
+
dy -= distance * F.cos(angle)
|
|
87
116
|
|
|
88
117
|
Diagonal.new(slope:, intercept: intercept - (slope * dx) + dy)
|
|
89
118
|
end
|
|
@@ -102,29 +131,61 @@ module Sevgi
|
|
|
102
131
|
# Evaluates x for a y coordinate.
|
|
103
132
|
# @param y [Numeric] y coordinate
|
|
104
133
|
# @return [Float]
|
|
105
|
-
|
|
134
|
+
# @raise [Sevgi::Geometry::Error] when y is not a finite real number
|
|
135
|
+
def x(y) = (Real[:y, y] - intercept) / slope
|
|
106
136
|
|
|
107
137
|
# Evaluates y for an x coordinate.
|
|
108
138
|
# @param x [Numeric] x coordinate
|
|
109
139
|
# @return [Float]
|
|
110
|
-
|
|
140
|
+
# @raise [Sevgi::Geometry::Error] when x is not a finite real number
|
|
141
|
+
def y(x) = Nonvertical.y(self, x)
|
|
111
142
|
|
|
112
143
|
alias == eql?
|
|
113
144
|
end
|
|
114
145
|
|
|
115
146
|
# Horizontal linear equation in `y = c` form.
|
|
116
|
-
class Horizontal <
|
|
147
|
+
class Horizontal < Linear
|
|
148
|
+
public_class_method :new
|
|
149
|
+
|
|
150
|
+
# Returns the zero line slope.
|
|
151
|
+
# @return [Float]
|
|
152
|
+
attr_reader :slope
|
|
153
|
+
|
|
154
|
+
# Returns the y coordinate.
|
|
155
|
+
# @return [Float]
|
|
156
|
+
attr_reader :intercept
|
|
157
|
+
|
|
117
158
|
# Creates a horizontal equation.
|
|
118
159
|
# @param c [Numeric] y coordinate
|
|
119
160
|
# @return [void]
|
|
120
161
|
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
121
|
-
def initialize(c)
|
|
162
|
+
def initialize(c)
|
|
163
|
+
super()
|
|
164
|
+
|
|
165
|
+
@slope = 0.0
|
|
166
|
+
@intercept = Real[:y, c]
|
|
167
|
+
end
|
|
122
168
|
|
|
123
169
|
# Returns an equation rounded to precision.
|
|
124
170
|
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
125
171
|
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
126
172
|
def approx(precision = nil) = self.class.new(F.approx(intercept, precision))
|
|
127
173
|
|
|
174
|
+
# Reports strict equation equality.
|
|
175
|
+
# @param other [Object] object to compare
|
|
176
|
+
# @return [Boolean]
|
|
177
|
+
def eql?(other) = Nonvertical.equal?(self, other)
|
|
178
|
+
|
|
179
|
+
# Returns a hash compatible with strict equality.
|
|
180
|
+
# @return [Integer]
|
|
181
|
+
def hash = Nonvertical.hash(self)
|
|
182
|
+
|
|
183
|
+
# Reports whether a point is on the line.
|
|
184
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
185
|
+
# @return [Boolean]
|
|
186
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
187
|
+
def on?(point) = Nonvertical.on?(self, point)
|
|
188
|
+
|
|
128
189
|
# Returns a parallel horizontal equation shifted by offsets.
|
|
129
190
|
#
|
|
130
191
|
# A positive signed distance shifts upward in screen coordinates.
|
|
@@ -132,19 +193,36 @@ module Sevgi
|
|
|
132
193
|
# @param dx [Numeric, nil] accepted for signature compatibility and ignored
|
|
133
194
|
# @param dy [Numeric, nil] explicit y translation
|
|
134
195
|
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
196
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
135
197
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
136
|
-
_dx = dx
|
|
198
|
+
distance, _dx, dy = shift_values(distance, dx, dy)
|
|
137
199
|
|
|
138
|
-
self.class.new(intercept +
|
|
200
|
+
self.class.new(intercept + dy - distance)
|
|
139
201
|
end
|
|
140
202
|
|
|
203
|
+
# Rejects x lookup because a horizontal equation does not determine one x coordinate.
|
|
204
|
+
# @param _y [Numeric] y coordinate
|
|
205
|
+
# @return [void]
|
|
206
|
+
# @raise [Sevgi::Geometry::Error] always, because x is indeterminate
|
|
207
|
+
def x(_y) = Error.("x is indeterminate for a horizontal equation")
|
|
208
|
+
|
|
141
209
|
# Formats the equation for display.
|
|
142
210
|
# @return [String]
|
|
143
211
|
def to_s = "Linear<y = #{F.approx(intercept)}>"
|
|
212
|
+
|
|
213
|
+
# Evaluates y for an x coordinate.
|
|
214
|
+
# @param x [Numeric] x coordinate
|
|
215
|
+
# @return [Float]
|
|
216
|
+
# @raise [Sevgi::Geometry::Error] when x is not a finite real number
|
|
217
|
+
def y(x) = Nonvertical.y(self, x)
|
|
218
|
+
|
|
219
|
+
alias == eql?
|
|
144
220
|
end
|
|
145
221
|
|
|
146
222
|
# Vertical linear equation in `x = c` form.
|
|
147
223
|
class Vertical < Linear
|
|
224
|
+
public_class_method :new
|
|
225
|
+
|
|
148
226
|
# Creates a vertical equation.
|
|
149
227
|
# @param c [Numeric] x coordinate
|
|
150
228
|
# @return [void]
|
|
@@ -169,16 +247,6 @@ module Sevgi
|
|
|
169
247
|
# @return [Integer]
|
|
170
248
|
def hash = [self.class, x].hash
|
|
171
249
|
|
|
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
250
|
# Reports whether a point is on the line.
|
|
183
251
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
184
252
|
# @return [Boolean]
|
|
@@ -189,16 +257,6 @@ module Sevgi
|
|
|
189
257
|
F.eq?(point.x, x(point.y))
|
|
190
258
|
end
|
|
191
259
|
|
|
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
260
|
# Returns a parallel vertical equation shifted by offsets.
|
|
203
261
|
#
|
|
204
262
|
# A positive signed distance shifts right in screen coordinates.
|
|
@@ -206,10 +264,11 @@ module Sevgi
|
|
|
206
264
|
# @param dx [Numeric, nil] explicit x translation
|
|
207
265
|
# @param dy [Numeric, nil] accepted for signature compatibility and ignored
|
|
208
266
|
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
267
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
209
268
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
210
|
-
_dy = dy
|
|
269
|
+
distance, dx, _dy = shift_values(distance, dx, dy)
|
|
211
270
|
|
|
212
|
-
self.class.new(x +
|
|
271
|
+
self.class.new(x + distance + dx)
|
|
213
272
|
end
|
|
214
273
|
|
|
215
274
|
# Formats the equation for display.
|
|
@@ -217,14 +276,19 @@ module Sevgi
|
|
|
217
276
|
def to_s = "Linear<x = #{F.approx(x)}>"
|
|
218
277
|
|
|
219
278
|
# Evaluates x for a y coordinate.
|
|
220
|
-
# @param
|
|
279
|
+
# @param y [Numeric, nil] ignored finite y coordinate
|
|
221
280
|
# @return [Float]
|
|
222
|
-
|
|
281
|
+
# @raise [Sevgi::Geometry::Error] when y is present and not a finite real number
|
|
282
|
+
def x(y = nil)
|
|
283
|
+
Real[:y, y] unless y.nil?
|
|
284
|
+
@x
|
|
285
|
+
end
|
|
223
286
|
|
|
224
287
|
# Evaluates y for an x coordinate.
|
|
225
|
-
# @param
|
|
226
|
-
# @return [
|
|
227
|
-
|
|
288
|
+
# @param _x [Numeric] x coordinate
|
|
289
|
+
# @return [void]
|
|
290
|
+
# @raise [Sevgi::Geometry::Error] always, because y is indeterminate
|
|
291
|
+
def y(_x) = Error.("y is indeterminate for a vertical equation")
|
|
228
292
|
|
|
229
293
|
alias == eql?
|
|
230
294
|
end
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
5
|
class Equation
|
|
6
|
-
# Base class for quadratic
|
|
6
|
+
# Base class reserved for future quadratic equation support.
|
|
7
|
+
# @api private
|
|
7
8
|
class Quadratic < Equation
|
|
8
9
|
end
|
|
10
|
+
|
|
11
|
+
private_constant :Quadratic
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
# Reserved circle element support.
|
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
#
|
|
5
|
+
# Abstract base class for geometry equations used in boundary intersections.
|
|
6
|
+
#
|
|
7
|
+
# The supported public factories build horizontal, vertical, and diagonal
|
|
8
|
+
# linear equations. {#intersect} always returns an Array: no intersection is
|
|
9
|
+
# `[]`, while one crossing is a one-item Array. Coincident parallel lines do
|
|
10
|
+
# not represent a finite intersection and also return an empty Array.
|
|
11
|
+
# @example Intersect two linear equations
|
|
12
|
+
# diagonal = Sevgi::Geometry::Equation.diagonal(slope: 1, intercept: 0)
|
|
13
|
+
# vertical = Sevgi::Geometry::Equation.vertical(3)
|
|
14
|
+
# diagonal.intersect(vertical).map(&:deconstruct) # => [[3.0, 3.0]]
|
|
15
|
+
# @see Sevgi::Geometry::Element::Lined#intersection
|
|
6
16
|
class Equation
|
|
17
|
+
private_class_method :new
|
|
18
|
+
|
|
7
19
|
# Builds a non-axis-aligned linear equation.
|
|
8
20
|
# @param slope [Numeric] line slope
|
|
9
21
|
# @param intercept [Numeric] y-intercept
|
|
10
22
|
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
11
|
-
# @raise [Sevgi::Geometry::Error] when a coefficient is not
|
|
23
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not finite Numeric or slope is zero
|
|
12
24
|
def self.diagonal(slope:, intercept:) = Linear::Diagonal.new(slope:, intercept:)
|
|
13
25
|
|
|
14
26
|
# Builds a horizontal linear equation.
|
|
@@ -55,19 +67,26 @@ module Sevgi
|
|
|
55
67
|
private
|
|
56
68
|
|
|
57
69
|
def linear_vs_linear(other)
|
|
58
|
-
case [self, other]
|
|
59
|
-
in [
|
|
60
|
-
diagonal_vs_diagonal(self, other)
|
|
61
|
-
in [Linear::Diagonal, Linear::Vertical]
|
|
62
|
-
diagonal_vs_vertical(self, other)
|
|
63
|
-
in [Linear::Vertical, Linear::Diagonal]
|
|
64
|
-
diagonal_vs_vertical(other, self)
|
|
65
|
-
in [Linear::Vertical, Linear::Vertical]
|
|
70
|
+
case [linear_category(self), linear_category(other)]
|
|
71
|
+
in [:vertical, :vertical]
|
|
66
72
|
nil
|
|
73
|
+
in [:vertical, :nonvertical]
|
|
74
|
+
nonvertical_vs_vertical(other, self)
|
|
75
|
+
in [:nonvertical, :vertical]
|
|
76
|
+
nonvertical_vs_vertical(self, other)
|
|
77
|
+
in [:nonvertical, :nonvertical]
|
|
78
|
+
nonvertical_vs_nonvertical(self, other)
|
|
67
79
|
end
|
|
68
80
|
end
|
|
69
81
|
|
|
70
|
-
def
|
|
82
|
+
def linear_category(equation)
|
|
83
|
+
return :vertical if equation.is_a?(Linear::Vertical)
|
|
84
|
+
return :nonvertical if equation.is_a?(Linear::Diagonal) || equation.is_a?(Linear::Horizontal)
|
|
85
|
+
|
|
86
|
+
PanicError.("Linear equation category not implemented: #{equation.class}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def nonvertical_vs_nonvertical(left, right)
|
|
71
90
|
return nil if F.eq?(left.slope, right.slope)
|
|
72
91
|
|
|
73
92
|
x = (right.intercept - left.intercept) / (left.slope - right.slope)
|
|
@@ -75,10 +94,10 @@ module Sevgi
|
|
|
75
94
|
Point[x, left.y(x)]
|
|
76
95
|
end
|
|
77
96
|
|
|
78
|
-
def
|
|
97
|
+
def nonvertical_vs_vertical(nonvertical, vertical)
|
|
79
98
|
x = vertical.x
|
|
80
99
|
|
|
81
|
-
Point[x,
|
|
100
|
+
Point[x, nonvertical.y(x)]
|
|
82
101
|
end
|
|
83
102
|
|
|
84
103
|
def linear_vs_quadratic(...)
|
|
@@ -92,6 +111,10 @@ module Sevgi
|
|
|
92
111
|
|
|
93
112
|
class Point
|
|
94
113
|
# Returns the linear equation passing through this point at an angle.
|
|
114
|
+
# @example Create axis-aligned equations through a point
|
|
115
|
+
# point = Sevgi::Geometry::Point[4, 3]
|
|
116
|
+
# point.equation(0).y(20) # => 3.0
|
|
117
|
+
# point.equation(90).x(20) # => 4.0
|
|
95
118
|
# @param angle [Numeric] clockwise angle in degrees
|
|
96
119
|
# @return [Sevgi::Geometry::Equation::Linear]
|
|
97
120
|
def equation(angle)
|
|
@@ -2,23 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
# Validates
|
|
5
|
+
# Validates numeric inputs that can be represented as finite Floats.
|
|
6
6
|
# @api private
|
|
7
7
|
module Real
|
|
8
|
-
# Coerces a public numeric value to Float.
|
|
8
|
+
# Coerces a public numeric value to a finite Float.
|
|
9
9
|
# @param field [Symbol, String] coordinate or component name
|
|
10
10
|
# @param value [Object] value to coerce
|
|
11
11
|
# @return [Float] finite float value
|
|
12
|
-
# @raise [Sevgi::Geometry::Error] when value is not a finite
|
|
12
|
+
# @raise [Sevgi::Geometry::Error] when value is not a Numeric, cannot be converted to Float, or is not finite
|
|
13
13
|
def self.[](field, value)
|
|
14
|
-
unless value.is_a?(::Numeric)
|
|
15
|
-
Error.("Geometry #{field} must be a finite Numeric: #{value.inspect}")
|
|
14
|
+
unless value.is_a?(::Numeric) && !value.is_a?(::Complex)
|
|
15
|
+
Error.("Geometry #{field} must be a finite real Numeric: #{value.inspect}")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
number = coerce(field, value)
|
|
19
|
+
|
|
20
|
+
unless number.is_a?(::Float) && number.finite?
|
|
21
|
+
Error.("Geometry #{field} must be finite: #{value.inspect}")
|
|
20
22
|
end
|
|
23
|
+
|
|
24
|
+
number
|
|
21
25
|
end
|
|
26
|
+
|
|
27
|
+
def self.coerce(field, value)
|
|
28
|
+
value.to_f
|
|
29
|
+
rescue ::StandardError => e
|
|
30
|
+
Error.("Geometry #{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_class_method :coerce
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
private_constant :Real
|
|
@@ -4,6 +4,7 @@ module Sevgi
|
|
|
4
4
|
module Geometry
|
|
5
5
|
module Operation
|
|
6
6
|
# Sweep operation implementation.
|
|
7
|
+
# @api private
|
|
7
8
|
module Sweep
|
|
8
9
|
extend self
|
|
9
10
|
|
|
@@ -13,7 +14,7 @@ module Sevgi
|
|
|
13
14
|
# Sweeps parallel lines across a lined element in both directions.
|
|
14
15
|
#
|
|
15
16
|
# Generated lines are boundary-to-boundary interior spans. A single
|
|
16
|
-
# sweep position can produce multiple lines for closed concave elements.
|
|
17
|
+
# sweep position can produce multiple lines for closed concave elements; open paths produce no interior lines.
|
|
17
18
|
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
18
19
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
19
20
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
@@ -23,9 +24,10 @@ module Sevgi
|
|
|
23
24
|
# @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
24
25
|
# @yieldreturn [void]
|
|
25
26
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
26
|
-
# @raise [Sevgi::Geometry::Error] when initial
|
|
27
|
+
# @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
27
28
|
# @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
28
29
|
def sweep(element, initial:, angle:, step:, limit: LIMIT, &block)
|
|
30
|
+
step = validate_arguments(step, limit)
|
|
29
31
|
equation = Tuple[Point, initial].equation(angle)
|
|
30
32
|
|
|
31
33
|
[
|
|
@@ -39,7 +41,7 @@ module Sevgi
|
|
|
39
41
|
# Sweeps parallel lines across a lined element and requires at least one result.
|
|
40
42
|
#
|
|
41
43
|
# Generated lines are boundary-to-boundary interior spans. A single
|
|
42
|
-
# sweep position can produce multiple lines for closed concave elements.
|
|
44
|
+
# sweep position can produce multiple lines for closed concave elements; open paths produce no interior lines.
|
|
43
45
|
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
44
46
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
45
47
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
@@ -49,7 +51,7 @@ module Sevgi
|
|
|
49
51
|
# @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
50
52
|
# @yieldreturn [void]
|
|
51
53
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
52
|
-
# @raise [Sevgi::Geometry::Error] when initial
|
|
54
|
+
# @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
53
55
|
# @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
|
|
54
56
|
def sweep!(element, initial:, angle:, step:, limit: LIMIT, &block)
|
|
55
57
|
sweep(element, initial:, angle:, step:, limit:) do |lines|
|
|
@@ -64,14 +66,16 @@ module Sevgi
|
|
|
64
66
|
# Sweeps parallel lines in one signed direction from an equation.
|
|
65
67
|
#
|
|
66
68
|
# Generated lines are boundary-to-boundary interior spans. A single
|
|
67
|
-
# sweep position can produce multiple lines for closed concave elements.
|
|
69
|
+
# sweep position can produce multiple lines for closed concave elements; open paths produce no interior lines.
|
|
68
70
|
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
69
71
|
# @param equation [Sevgi::Geometry::Equation] initial sweep equation
|
|
70
72
|
# @param step [Numeric] signed distance between sweep lines
|
|
71
73
|
# @param limit [Integer] maximum iterations
|
|
72
74
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
75
|
+
# @raise [Sevgi::Geometry::Error] when step or limit is invalid
|
|
73
76
|
# @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
74
77
|
def unisweep(element, equation, step, limit: LIMIT)
|
|
78
|
+
step = validate_arguments(step, limit)
|
|
75
79
|
lines = []
|
|
76
80
|
|
|
77
81
|
limit.times do
|
|
@@ -86,6 +90,8 @@ module Sevgi
|
|
|
86
90
|
OperationError.("Loop limit reached: #{limit}")
|
|
87
91
|
end
|
|
88
92
|
|
|
93
|
+
private :unisweep
|
|
94
|
+
|
|
89
95
|
# Reports whether the sweep handler can operate on an element.
|
|
90
96
|
# @api private
|
|
91
97
|
# @param element [Object] candidate element
|
|
@@ -96,7 +102,19 @@ module Sevgi
|
|
|
96
102
|
|
|
97
103
|
private
|
|
98
104
|
|
|
105
|
+
def validate_arguments(step, limit)
|
|
106
|
+
step = Real[:step, step]
|
|
107
|
+
Error.("Sweep step must be nonzero") if step.zero?
|
|
108
|
+
unless limit.is_a?(::Integer) && limit.positive?
|
|
109
|
+
Error.("Sweep limit must be a positive Integer: #{limit.inspect}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
step
|
|
113
|
+
end
|
|
114
|
+
|
|
99
115
|
def interior_lines(element, equation, points)
|
|
116
|
+
return [] unless element.class.send(:close?)
|
|
117
|
+
|
|
100
118
|
if points.size == 2
|
|
101
119
|
line = simple_line(points)
|
|
102
120
|
|
|
@@ -127,7 +145,7 @@ module Sevgi
|
|
|
127
145
|
end
|
|
128
146
|
end
|
|
129
147
|
|
|
130
|
-
register(Sweep, :sweep, :sweep
|
|
148
|
+
register(Sweep, :sweep, :sweep!)
|
|
131
149
|
|
|
132
150
|
private_constant :Sweep
|
|
133
151
|
end
|