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.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/sevgi/geometry/element.rb +211 -31
- data/lib/sevgi/geometry/elements/line.rb +47 -8
- data/lib/sevgi/geometry/elements/parallelogram.rb +59 -0
- data/lib/sevgi/geometry/elements/polygon.rb +7 -1
- data/lib/sevgi/geometry/elements/polyline.rb +7 -1
- data/lib/sevgi/geometry/elements/rect.rb +56 -8
- data/lib/sevgi/geometry/elements/triangle.rb +43 -0
- data/lib/sevgi/geometry/equation/linear.rb +147 -7
- data/lib/sevgi/geometry/equation/quadratic.rb +7 -1
- data/lib/sevgi/geometry/equation.rb +47 -9
- data/lib/sevgi/geometry/errors.rb +1 -0
- data/lib/sevgi/geometry/internal.rb +14 -0
- data/lib/sevgi/geometry/operation/align.rb +35 -9
- data/lib/sevgi/geometry/operation/sweep.rb +42 -5
- data/lib/sevgi/geometry/operation.rb +9 -0
- data/lib/sevgi/geometry/point.rb +103 -6
- data/lib/sevgi/geometry/segment.rb +87 -13
- data/lib/sevgi/geometry/version.rb +2 -1
- data/lib/sevgi/geometry.rb +9 -0
- metadata +5 -5
- data/lib/sevgi/geometry/elements/parm.rb +0 -37
- data/lib/sevgi/geometry/elements/tri.rb +0 -18
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
PanicError.("Intersection not implemented: #{self.class} / #{other.class}")
|
|
24
40
|
end
|
|
25
41
|
|
|
26
42
|
Array(points)
|
|
27
43
|
end
|
|
28
44
|
|
|
29
|
-
|
|
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
|
-
|
|
57
|
+
diagonal_vs_diagonal(self, other)
|
|
37
58
|
in [Linear::Diagonal, Linear::Vertical]
|
|
38
|
-
|
|
59
|
+
diagonal_vs_vertical(self, other)
|
|
39
60
|
in [Linear::Vertical, Linear::Diagonal]
|
|
40
|
-
|
|
61
|
+
diagonal_vs_vertical(other, self)
|
|
41
62
|
in [Linear::Vertical, Linear::Vertical]
|
|
42
|
-
|
|
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
|
-
|
|
82
|
+
PanicError.("Linear/quadratic intersection must be implemented")
|
|
50
83
|
end
|
|
51
84
|
|
|
52
85
|
def quadratic_vs_quadratic(...)
|
|
53
|
-
|
|
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
|
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
Point[that.position.x - this.position.x, 0]
|
|
19
43
|
when :right
|
|
20
|
-
|
|
44
|
+
Point[(that.position.x + that.width) - (this.position.x + this.width), 0]
|
|
21
45
|
when :top
|
|
22
|
-
|
|
46
|
+
Point[0, that.position.y - this.position.y]
|
|
23
47
|
when :bottom
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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},
|
|
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
|