sevgi-geometry 0.95.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +221 -2
- data/README.md +33 -8
- data/lib/sevgi/geometry/element.rb +320 -161
- data/lib/sevgi/geometry/elements/arc.rb +256 -0
- data/lib/sevgi/geometry/elements/circle.rb +28 -0
- data/lib/sevgi/geometry/elements/ellipse/affine.rb +72 -0
- data/lib/sevgi/geometry/elements/ellipse/length.rb +80 -0
- data/lib/sevgi/geometry/elements/ellipse.rb +269 -0
- data/lib/sevgi/geometry/elements/line.rb +68 -23
- data/lib/sevgi/geometry/elements/parallelogram.rb +108 -29
- data/lib/sevgi/geometry/elements/polygon.rb +41 -1
- data/lib/sevgi/geometry/elements/polyline.rb +45 -1
- data/lib/sevgi/geometry/elements/rect.rb +163 -29
- data/lib/sevgi/geometry/elements/triangle.rb +57 -11
- data/lib/sevgi/geometry/equation/linear.rb +131 -69
- data/lib/sevgi/geometry/equation/quadratic.rb +98 -16
- data/lib/sevgi/geometry/equation.rb +65 -24
- data/lib/sevgi/geometry/internal.rb +11 -3
- data/lib/sevgi/geometry/operation/align.rb +2 -4
- data/lib/sevgi/geometry/operation/box.rb +50 -0
- data/lib/sevgi/geometry/operation/sweep.rb +32 -18
- data/lib/sevgi/geometry/operation.rb +33 -18
- data/lib/sevgi/geometry/point.rb +126 -14
- data/lib/sevgi/geometry/predicate.rb +176 -0
- data/lib/sevgi/geometry/segment.rb +79 -14
- data/lib/sevgi/geometry/version.rb +1 -1
- data/lib/sevgi/geometry.rb +26 -2
- metadata +13 -6
|
@@ -7,8 +7,52 @@ module Sevgi
|
|
|
7
7
|
PolylineBase = Element.lined(open: true)
|
|
8
8
|
private_constant :PolylineBase
|
|
9
9
|
|
|
10
|
-
# Variable-size open lined element.
|
|
10
|
+
# Variable-size open lined element with at least two points.
|
|
11
|
+
# @!method self.[](*segments, position: Origin)
|
|
12
|
+
# Builds a polyline from ordered segments.
|
|
13
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] ordered segments
|
|
14
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
15
|
+
# @return [Sevgi::Geometry::Polyline]
|
|
16
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or do not form a polyline
|
|
17
|
+
# @!method self.call(*points)
|
|
18
|
+
# Builds a polyline from ordered points.
|
|
19
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] ordered points
|
|
20
|
+
# @return [Sevgi::Geometry::Polyline]
|
|
21
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or do not form a polyline
|
|
22
|
+
# @!method self.from_segments(*segments, position: Origin)
|
|
23
|
+
# Builds a polyline from ordered segments.
|
|
24
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] ordered segments
|
|
25
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
26
|
+
# @return [Sevgi::Geometry::Polyline]
|
|
27
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or do not form a polyline
|
|
28
|
+
# @!method self.from_points(*points)
|
|
29
|
+
# Builds a polyline from ordered points.
|
|
30
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] ordered points
|
|
31
|
+
# @return [Sevgi::Geometry::Polyline]
|
|
32
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or do not form a polyline
|
|
33
|
+
# @!method starting
|
|
34
|
+
# Returns the first point in the directed path.
|
|
35
|
+
# @return [Sevgi::Geometry::Point]
|
|
36
|
+
# @!method ending
|
|
37
|
+
# Returns the last point in the directed path.
|
|
38
|
+
# @return [Sevgi::Geometry::Point]
|
|
39
|
+
# @!method reverse
|
|
40
|
+
# Returns the same trace with opposite traversal.
|
|
41
|
+
# @return [Sevgi::Geometry::Polyline]
|
|
42
|
+
# @example Pair mathematical notation with English conveniences
|
|
43
|
+
# Sevgi::Geometry::Polyline[[2, 0], [1, 90]] == Sevgi::Geometry::Polyline.from_segments([2, 0], [1, 90])
|
|
44
|
+
# Sevgi::Geometry::Polyline.([0, 0], [2, 0]) == Sevgi::Geometry::Polyline.from_points([0, 0], [2, 0])
|
|
45
|
+
# @example Measure and query an open path
|
|
46
|
+
# path = Sevgi::Geometry::Polyline.([0, 0], [3, 0], [3, 4])
|
|
47
|
+
# path.length # => 7.0
|
|
48
|
+
# path.on?([2, 0]) # => true
|
|
49
|
+
# path.inside?([1, 1]) # => false
|
|
11
50
|
class Polyline < PolylineBase
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_geometry!
|
|
54
|
+
Error.("Polyline requires at least two points") if points.size < 2
|
|
55
|
+
end
|
|
12
56
|
end
|
|
13
57
|
end
|
|
14
58
|
end
|
|
@@ -7,7 +7,36 @@ module Sevgi
|
|
|
7
7
|
RectBase = Element.lined(4)
|
|
8
8
|
private_constant :RectBase
|
|
9
9
|
|
|
10
|
-
# Closed four-sided rectangle aligned to the screen axes.
|
|
10
|
+
# Closed four-sided rectangle aligned to the screen axes. Affine operations return Rect while the result remains
|
|
11
|
+
# axis-aligned and widen to {Parallelogram} after rotation or skew changes that category. A Square similarly widens
|
|
12
|
+
# to Rect after unequal scaling.
|
|
13
|
+
# @example Inspect corners, sides, and containment
|
|
14
|
+
# rect = Sevgi::Geometry::Rect[8, 4, position: [2, 3]]
|
|
15
|
+
# rect.top_left.deconstruct # => [2.0, 3.0]
|
|
16
|
+
# rect.right.length # => 4.0
|
|
17
|
+
# rect.inside?([5, 5]) # => true
|
|
18
|
+
# @example Observe semantic widening after affine transforms
|
|
19
|
+
# Sevgi::Geometry::Rect[8, 4].rotate(30).class # => Sevgi::Geometry::Parallelogram
|
|
20
|
+
# Sevgi::Geometry::Square[4].scale(2, 1).class # => Sevgi::Geometry::Rect
|
|
21
|
+
# @!attribute [r] A
|
|
22
|
+
# @return [Sevgi::Geometry::Point] top-left vertex
|
|
23
|
+
# @!attribute [r] B
|
|
24
|
+
# @return [Sevgi::Geometry::Point] top-right vertex
|
|
25
|
+
# @!attribute [r] C
|
|
26
|
+
# @return [Sevgi::Geometry::Point] bottom-right vertex
|
|
27
|
+
# @!attribute [r] D
|
|
28
|
+
# @return [Sevgi::Geometry::Point] bottom-left vertex
|
|
29
|
+
# @!attribute [r] AB
|
|
30
|
+
# @return [Sevgi::Geometry::Line] top side
|
|
31
|
+
# @!attribute [r] BC
|
|
32
|
+
# @return [Sevgi::Geometry::Line] right side
|
|
33
|
+
# @!attribute [r] CD
|
|
34
|
+
# @return [Sevgi::Geometry::Line] bottom side
|
|
35
|
+
# @!attribute [r] DA
|
|
36
|
+
# @return [Sevgi::Geometry::Line] left side
|
|
37
|
+
# @!method perimeter
|
|
38
|
+
# Returns the closed path perimeter.
|
|
39
|
+
# @return [Float]
|
|
11
40
|
class Rect < RectBase
|
|
12
41
|
# @overload [](width, height, position: Origin)
|
|
13
42
|
# Builds a rectangle from size and top-left position.
|
|
@@ -15,8 +44,30 @@ module Sevgi
|
|
|
15
44
|
# @param height [Numeric] rectangle height
|
|
16
45
|
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
17
46
|
# @return [Sevgi::Geometry::Rect]
|
|
18
|
-
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
19
|
-
|
|
47
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
|
|
48
|
+
# @example Mathematical notation and English convenience are equivalent
|
|
49
|
+
# Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
|
|
50
|
+
def self.[](width, height, position: Origin) = construct(width, height, position:)
|
|
51
|
+
|
|
52
|
+
# Constructs a rectangle for canonical size notation.
|
|
53
|
+
# @param width [Numeric] rectangle width
|
|
54
|
+
# @param height [Numeric] rectangle height
|
|
55
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
56
|
+
# @return [Sevgi::Geometry::Rect]
|
|
57
|
+
# @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
|
|
58
|
+
# @api private
|
|
59
|
+
def self.construct(width, height, position:)
|
|
60
|
+
width = dimension!(:width, width)
|
|
61
|
+
height = dimension!(:height, height)
|
|
62
|
+
|
|
63
|
+
new_by_segments(
|
|
64
|
+
Segment.rightward(width),
|
|
65
|
+
Segment.downward(height),
|
|
66
|
+
Segment.leftward(width),
|
|
67
|
+
Segment.upward(height),
|
|
68
|
+
position:
|
|
69
|
+
)
|
|
70
|
+
end
|
|
20
71
|
|
|
21
72
|
# @overload call(top_left, bottom_right)
|
|
22
73
|
# Builds a rectangle from two opposite corners.
|
|
@@ -24,46 +75,96 @@ module Sevgi
|
|
|
24
75
|
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
25
76
|
# @return [Sevgi::Geometry::Rect]
|
|
26
77
|
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
27
|
-
|
|
78
|
+
# @example Mathematical notation and English convenience are equivalent
|
|
79
|
+
# Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
|
|
80
|
+
def self.call(top_left, bottom_right)
|
|
81
|
+
top_left, bottom_right = Tuples[Point, top_left, bottom_right]
|
|
82
|
+
left, right = [top_left.x, bottom_right.x].minmax
|
|
83
|
+
top, bottom = [top_left.y, bottom_right.y].minmax
|
|
84
|
+
width, height = right - left, bottom - top
|
|
85
|
+
|
|
86
|
+
if self <= Square
|
|
87
|
+
Error.("Square corners must define equal dimensions") unless F.eq?(width, height)
|
|
88
|
+
|
|
89
|
+
return self[width, position: [left, top]]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
self[width, height, position: [left, top]]
|
|
93
|
+
end
|
|
28
94
|
|
|
29
95
|
# Builds a rectangle from two opposite corners.
|
|
30
96
|
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
31
97
|
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
32
98
|
# @return [Sevgi::Geometry::Rect]
|
|
33
99
|
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
34
|
-
def self.from_corners(top_left, bottom_right)
|
|
35
|
-
top_left, bottom_right = Tuples[Point, top_left, bottom_right]
|
|
36
|
-
width = (bottom_right.x - top_left.x).abs
|
|
37
|
-
|
|
38
|
-
new_by_points(
|
|
39
|
-
top_left,
|
|
40
|
-
top_left.translate(width, 0.0),
|
|
41
|
-
bottom_right,
|
|
42
|
-
bottom_right.translate(-width, 0.0)
|
|
43
|
-
)
|
|
44
|
-
end
|
|
100
|
+
def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)
|
|
45
101
|
|
|
46
102
|
# Builds a rectangle from size and top-left position.
|
|
47
103
|
# @param width [Numeric] rectangle width
|
|
48
104
|
# @param height [Numeric] rectangle height
|
|
49
105
|
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
50
106
|
# @return [Sevgi::Geometry::Rect]
|
|
51
|
-
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
52
|
-
def self.from_size(width, height, position: Origin)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
107
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
|
|
108
|
+
def self.from_size(width, height, position: Origin) = self[width, height, position:]
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def affine(*points)
|
|
114
|
+
left, right, top, bottom = bounds(points)
|
|
115
|
+
width, height = right - left, bottom - top
|
|
116
|
+
|
|
117
|
+
unless axis_aligned?(points, left, right, top, bottom)
|
|
118
|
+
return Parallelogram.call(*points.first(4))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
klass = self <= Square && F.eq?(width, height) ? Square : Rect
|
|
122
|
+
return klass[width, position: [left, top]] if klass == Square
|
|
123
|
+
|
|
124
|
+
klass[width, height, position: [left, top]]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def approximate(*points)
|
|
128
|
+
new_by_points!(*points)
|
|
129
|
+
rescue Error
|
|
130
|
+
return Rect.send(:new_by_points!, *points) if self <= Square
|
|
131
|
+
|
|
132
|
+
super
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def axis_aligned?(points, left, right, top, bottom)
|
|
136
|
+
expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
|
|
137
|
+
vertices = points.first(4)
|
|
138
|
+
|
|
139
|
+
expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def bounds(points)
|
|
143
|
+
vertices = points.first(4)
|
|
144
|
+
[vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def dimension!(name, value)
|
|
148
|
+
value = Real[name, value]
|
|
149
|
+
Error.("Rectangle #{name} cannot be negative") if value.negative?
|
|
150
|
+
|
|
151
|
+
value
|
|
152
|
+
end
|
|
60
153
|
end
|
|
61
154
|
|
|
155
|
+
private_class_method :construct, :from_points, :from_segments
|
|
156
|
+
|
|
62
157
|
# Draws the rectangle into a graphics node.
|
|
63
158
|
# @param node [Object] graphics node receiving the drawing command
|
|
64
159
|
# @return [Object] graphics node command result
|
|
65
160
|
def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)
|
|
66
161
|
|
|
162
|
+
private :draw!
|
|
163
|
+
|
|
164
|
+
# Returns the rectangle center.
|
|
165
|
+
# @return [Sevgi::Geometry::Point]
|
|
166
|
+
def center = Point.midpoint(top_left, bottom_right)
|
|
167
|
+
|
|
67
168
|
# Returns rectangle height.
|
|
68
169
|
# @return [Float]
|
|
69
170
|
def height = @height ||= segments[1].length
|
|
@@ -111,19 +212,52 @@ module Sevgi
|
|
|
111
212
|
%i[top right bottom left].each_with_index do |side, i|
|
|
112
213
|
define_method(side) { lines[i] }
|
|
113
214
|
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def validate_geometry!
|
|
219
|
+
left, right, top, bottom = self.class.send(:bounds, points)
|
|
220
|
+
expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
|
|
221
|
+
valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }
|
|
222
|
+
|
|
223
|
+
Error.("Rectangle points must form an axis-aligned rectangle") unless valid
|
|
224
|
+
end
|
|
114
225
|
end
|
|
115
226
|
|
|
116
|
-
# Rectangle with equal width and height.
|
|
227
|
+
# Rectangle with equal width and height. Use {#width} or {#height} for its side length. Inherited
|
|
228
|
+
# {Element::Lined#length} returns the complete path length.
|
|
229
|
+
# @example Construct the same square from opposite corners
|
|
230
|
+
# Sevgi::Geometry::Square.([0, 0], [5, 5]) == Sevgi::Geometry::Square.from_corners([0, 0], [5, 5])
|
|
117
231
|
class Square < Rect
|
|
118
|
-
#
|
|
119
|
-
|
|
232
|
+
# Builds a square from side length and top-left position.
|
|
233
|
+
# @param length [Numeric] side length
|
|
234
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
235
|
+
# @return [Sevgi::Geometry::Square]
|
|
236
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced or length is negative
|
|
237
|
+
# @example Mathematical notation and English convenience are equivalent
|
|
238
|
+
# Sevgi::Geometry::Square[5] == Sevgi::Geometry::Square.from_size(5)
|
|
239
|
+
def self.[](length, position: Origin) = construct(length, length, position:)
|
|
240
|
+
|
|
241
|
+
# Builds a square from two opposite corners.
|
|
242
|
+
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
243
|
+
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
244
|
+
# @return [Sevgi::Geometry::Square]
|
|
245
|
+
# @raise [Sevgi::Geometry::Error] when points are invalid or define unequal dimensions
|
|
246
|
+
def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)
|
|
120
247
|
|
|
121
248
|
# Builds a square from side length and top-left position.
|
|
122
249
|
# @param length [Numeric] side length
|
|
123
250
|
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
124
251
|
# @return [Sevgi::Geometry::Square]
|
|
125
|
-
# @raise [Sevgi::Geometry::Error] when position
|
|
126
|
-
def self.
|
|
252
|
+
# @raise [Sevgi::Geometry::Error] when position or length is invalid
|
|
253
|
+
def self.from_size(length, position: Origin) = self[length, position:]
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def validate_geometry!
|
|
258
|
+
super
|
|
259
|
+
Error.("Square sides must have equal length") unless F.eq?(width, height)
|
|
260
|
+
end
|
|
127
261
|
end
|
|
128
262
|
end
|
|
129
263
|
end
|
|
@@ -7,12 +7,53 @@ 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
|
#
|
|
14
55
|
# The closing segment is the direct vector from the end of `segment_b`
|
|
15
|
-
# back to `position`. Segment order controls orientation
|
|
56
|
+
# back to `position`. Segment order controls orientation. Reversing the
|
|
16
57
|
# inputs returns the corresponding opposite orientation. Zero-length or
|
|
17
58
|
# collinear inputs are rejected using the current numeric precision.
|
|
18
59
|
# @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first segment
|
|
@@ -27,21 +68,26 @@ 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 validate!(a, b)
|
|
79
|
+
middle = a.ending(Origin)
|
|
80
|
+
return unless F.zero?(a.length) || F.zero?(b.length) || Point.collinear?(Origin, middle, b.ending(middle))
|
|
35
81
|
|
|
36
|
-
def self.validate!(a, b)
|
|
37
|
-
if F.zero?(a.length) ||
|
|
38
|
-
F.zero?(b.length) ||
|
|
39
|
-
F.zero?(cross(a, b))
|
|
40
82
|
Error.("Triangle segments must form a non-degenerate triangle")
|
|
41
83
|
end
|
|
42
84
|
end
|
|
43
85
|
|
|
44
|
-
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def validate_geometry!
|
|
89
|
+
self.class.send(:validate!, segments[0], segments[1])
|
|
90
|
+
end
|
|
45
91
|
end
|
|
46
92
|
end
|
|
47
93
|
end
|