sevgi-geometry 0.95.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 +125 -0
- data/README.md +7 -7
- data/lib/sevgi/geometry/element.rb +222 -117
- data/lib/sevgi/geometry/elements/line.rb +60 -15
- 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 +131 -69
- data/lib/sevgi/geometry/equation.rb +36 -13
- data/lib/sevgi/geometry/internal.rb +3 -3
- data/lib/sevgi/geometry/operation/align.rb +1 -0
- data/lib/sevgi/geometry/operation/sweep.rb +20 -4
- data/lib/sevgi/geometry/operation.rb +28 -14
- data/lib/sevgi/geometry/point.rb +115 -13
- data/lib/sevgi/geometry/segment.rb +76 -13
- 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,10 +3,61 @@
|
|
|
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
|
|
59
|
+
public_class_method :new
|
|
60
|
+
|
|
10
61
|
# Returns the line slope.
|
|
11
62
|
# @return [Float]
|
|
12
63
|
attr_reader :slope
|
|
@@ -19,12 +70,13 @@ module Sevgi
|
|
|
19
70
|
# @param slope [Numeric] line slope
|
|
20
71
|
# @param intercept [Numeric] y-intercept
|
|
21
72
|
# @return [void]
|
|
22
|
-
# @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
|
|
23
74
|
def initialize(slope:, intercept:)
|
|
24
75
|
super()
|
|
25
76
|
|
|
26
77
|
@slope = Real[:slope, slope]
|
|
27
78
|
@intercept = Real[:intercept, intercept]
|
|
79
|
+
Error.("A diagonal equation requires a non-zero slope") if @slope.zero?
|
|
28
80
|
end
|
|
29
81
|
|
|
30
82
|
# Returns an equation rounded to precision.
|
|
@@ -37,55 +89,30 @@ module Sevgi
|
|
|
37
89
|
# Reports strict equation equality.
|
|
38
90
|
# @param other [Object] object to compare
|
|
39
91
|
# @return [Boolean]
|
|
40
|
-
def eql?(other) = self
|
|
92
|
+
def eql?(other) = Nonvertical.equal?(self, other)
|
|
41
93
|
|
|
42
94
|
# Returns a hash compatible with strict equality.
|
|
43
95
|
# @return [Integer]
|
|
44
|
-
def hash =
|
|
45
|
-
|
|
46
|
-
# Reports whether a point is on the left side of the line in screen coordinates.
|
|
47
|
-
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
48
|
-
# @return [Boolean]
|
|
49
|
-
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
50
|
-
def left?(point)
|
|
51
|
-
point = Tuple[Point, point]
|
|
52
|
-
|
|
53
|
-
F.gt?(point.y, y(point.x))
|
|
54
|
-
end
|
|
96
|
+
def hash = Nonvertical.hash(self)
|
|
55
97
|
|
|
56
98
|
# Reports whether a point is on the line.
|
|
57
99
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
58
100
|
# @return [Boolean]
|
|
59
101
|
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
60
|
-
def on?(point)
|
|
61
|
-
point = Tuple[Point, point]
|
|
62
|
-
|
|
63
|
-
F.eq?(point.y, y(point.x))
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Reports whether a point is on the right side of the line in screen coordinates.
|
|
67
|
-
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
68
|
-
# @return [Boolean]
|
|
69
|
-
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
70
|
-
def right?(point)
|
|
71
|
-
point = Tuple[Point, point]
|
|
72
|
-
|
|
73
|
-
F.lt?(point.y, y(point.x))
|
|
74
|
-
end
|
|
102
|
+
def on?(point) = Nonvertical.on?(self, point)
|
|
75
103
|
|
|
76
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.
|
|
77
106
|
# @param distance [Numeric, nil] signed perpendicular offset
|
|
78
107
|
# @param dx [Numeric, nil] explicit x translation
|
|
79
108
|
# @param dy [Numeric, nil] explicit y translation
|
|
80
109
|
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
110
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
81
111
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
82
|
-
dx
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
dx += distance * F.sin(angle = F.atan(slope))
|
|
87
|
-
dy -= distance * F.cos(angle)
|
|
88
|
-
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)
|
|
89
116
|
|
|
90
117
|
Diagonal.new(slope:, intercept: intercept - (slope * dx) + dy)
|
|
91
118
|
end
|
|
@@ -104,29 +131,61 @@ module Sevgi
|
|
|
104
131
|
# Evaluates x for a y coordinate.
|
|
105
132
|
# @param y [Numeric] y coordinate
|
|
106
133
|
# @return [Float]
|
|
107
|
-
|
|
134
|
+
# @raise [Sevgi::Geometry::Error] when y is not a finite real number
|
|
135
|
+
def x(y) = (Real[:y, y] - intercept) / slope
|
|
108
136
|
|
|
109
137
|
# Evaluates y for an x coordinate.
|
|
110
138
|
# @param x [Numeric] x coordinate
|
|
111
139
|
# @return [Float]
|
|
112
|
-
|
|
140
|
+
# @raise [Sevgi::Geometry::Error] when x is not a finite real number
|
|
141
|
+
def y(x) = Nonvertical.y(self, x)
|
|
113
142
|
|
|
114
143
|
alias == eql?
|
|
115
144
|
end
|
|
116
145
|
|
|
117
146
|
# Horizontal linear equation in `y = c` form.
|
|
118
|
-
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
|
+
|
|
119
158
|
# Creates a horizontal equation.
|
|
120
159
|
# @param c [Numeric] y coordinate
|
|
121
160
|
# @return [void]
|
|
122
161
|
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
123
|
-
def initialize(c)
|
|
162
|
+
def initialize(c)
|
|
163
|
+
super()
|
|
164
|
+
|
|
165
|
+
@slope = 0.0
|
|
166
|
+
@intercept = Real[:y, c]
|
|
167
|
+
end
|
|
124
168
|
|
|
125
169
|
# Returns an equation rounded to precision.
|
|
126
170
|
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
127
171
|
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
128
172
|
def approx(precision = nil) = self.class.new(F.approx(intercept, precision))
|
|
129
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
|
+
|
|
130
189
|
# Returns a parallel horizontal equation shifted by offsets.
|
|
131
190
|
#
|
|
132
191
|
# A positive signed distance shifts upward in screen coordinates.
|
|
@@ -134,19 +193,36 @@ module Sevgi
|
|
|
134
193
|
# @param dx [Numeric, nil] accepted for signature compatibility and ignored
|
|
135
194
|
# @param dy [Numeric, nil] explicit y translation
|
|
136
195
|
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
196
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
137
197
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
138
|
-
_dx = dx
|
|
198
|
+
distance, _dx, dy = shift_values(distance, dx, dy)
|
|
139
199
|
|
|
140
|
-
self.class.new(intercept +
|
|
200
|
+
self.class.new(intercept + dy - distance)
|
|
141
201
|
end
|
|
142
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
|
+
|
|
143
209
|
# Formats the equation for display.
|
|
144
210
|
# @return [String]
|
|
145
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?
|
|
146
220
|
end
|
|
147
221
|
|
|
148
222
|
# Vertical linear equation in `x = c` form.
|
|
149
223
|
class Vertical < Linear
|
|
224
|
+
public_class_method :new
|
|
225
|
+
|
|
150
226
|
# Creates a vertical equation.
|
|
151
227
|
# @param c [Numeric] x coordinate
|
|
152
228
|
# @return [void]
|
|
@@ -171,16 +247,6 @@ module Sevgi
|
|
|
171
247
|
# @return [Integer]
|
|
172
248
|
def hash = [self.class, x].hash
|
|
173
249
|
|
|
174
|
-
# Reports whether a point is on the left side of the line.
|
|
175
|
-
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
176
|
-
# @return [Boolean]
|
|
177
|
-
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
178
|
-
def left?(point)
|
|
179
|
-
point = Tuple[Point, point]
|
|
180
|
-
|
|
181
|
-
F.lt?(point.x, x(point.y))
|
|
182
|
-
end
|
|
183
|
-
|
|
184
250
|
# Reports whether a point is on the line.
|
|
185
251
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
186
252
|
# @return [Boolean]
|
|
@@ -191,16 +257,6 @@ module Sevgi
|
|
|
191
257
|
F.eq?(point.x, x(point.y))
|
|
192
258
|
end
|
|
193
259
|
|
|
194
|
-
# Reports whether a point is on the right side of the line.
|
|
195
|
-
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
196
|
-
# @return [Boolean]
|
|
197
|
-
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
198
|
-
def right?(point)
|
|
199
|
-
point = Tuple[Point, point]
|
|
200
|
-
|
|
201
|
-
F.gt?(point.x, x(point.y))
|
|
202
|
-
end
|
|
203
|
-
|
|
204
260
|
# Returns a parallel vertical equation shifted by offsets.
|
|
205
261
|
#
|
|
206
262
|
# A positive signed distance shifts right in screen coordinates.
|
|
@@ -208,10 +264,11 @@ module Sevgi
|
|
|
208
264
|
# @param dx [Numeric, nil] explicit x translation
|
|
209
265
|
# @param dy [Numeric, nil] accepted for signature compatibility and ignored
|
|
210
266
|
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
267
|
+
# @raise [Sevgi::Geometry::Error] when an operand is not a finite real number
|
|
211
268
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
212
|
-
_dy = dy
|
|
269
|
+
distance, dx, _dy = shift_values(distance, dx, dy)
|
|
213
270
|
|
|
214
|
-
self.class.new(x +
|
|
271
|
+
self.class.new(x + distance + dx)
|
|
215
272
|
end
|
|
216
273
|
|
|
217
274
|
# Formats the equation for display.
|
|
@@ -219,14 +276,19 @@ module Sevgi
|
|
|
219
276
|
def to_s = "Linear<x = #{F.approx(x)}>"
|
|
220
277
|
|
|
221
278
|
# Evaluates x for a y coordinate.
|
|
222
|
-
# @param
|
|
279
|
+
# @param y [Numeric, nil] ignored finite y coordinate
|
|
223
280
|
# @return [Float]
|
|
224
|
-
|
|
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
|
|
225
286
|
|
|
226
287
|
# Evaluates y for an x coordinate.
|
|
227
|
-
# @param
|
|
228
|
-
# @return [
|
|
229
|
-
|
|
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")
|
|
230
292
|
|
|
231
293
|
alias == eql?
|
|
232
294
|
end
|
|
@@ -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)
|
|
@@ -11,8 +11,8 @@ module Sevgi
|
|
|
11
11
|
# @return [Float] finite float value
|
|
12
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
18
|
number = coerce(field, value)
|
|
@@ -27,7 +27,7 @@ module Sevgi
|
|
|
27
27
|
def self.coerce(field, value)
|
|
28
28
|
value.to_f
|
|
29
29
|
rescue ::StandardError => e
|
|
30
|
-
Error.("Geometry #{field} must be a finite Numeric: #{value.inspect} (#{e.message})")
|
|
30
|
+
Error.("Geometry #{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private_class_method :coerce
|
|
@@ -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
|
|
|
@@ -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
|
[
|
|
@@ -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|
|
|
@@ -70,8 +72,10 @@ module Sevgi
|
|
|
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,8 +102,18 @@ 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)
|
|
100
|
-
return []
|
|
116
|
+
return [] unless element.class.send(:close?)
|
|
101
117
|
|
|
102
118
|
if points.size == 2
|
|
103
119
|
line = simple_line(points)
|
|
@@ -129,7 +145,7 @@ module Sevgi
|
|
|
129
145
|
end
|
|
130
146
|
end
|
|
131
147
|
|
|
132
|
-
register(Sweep, :sweep, :sweep
|
|
148
|
+
register(Sweep, :sweep, :sweep!)
|
|
133
149
|
|
|
134
150
|
private_constant :Sweep
|
|
135
151
|
end
|