sevgi-geometry 0.93.1 → 0.95.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 +152 -0
- data/LICENSE +674 -0
- data/README.md +34 -2
- data/lib/sevgi/geometry/element.rb +106 -28
- data/lib/sevgi/geometry/elements/line.rb +24 -9
- data/lib/sevgi/geometry/elements/rect.rb +32 -0
- data/lib/sevgi/geometry/elements/triangle.rb +17 -13
- data/lib/sevgi/geometry/equation/linear.rb +13 -8
- data/lib/sevgi/geometry/equation/quadratic.rb +8 -2
- data/lib/sevgi/geometry/equation.rb +3 -0
- data/lib/sevgi/geometry/internal.rb +34 -2
- data/lib/sevgi/geometry/operation/align.rb +2 -0
- data/lib/sevgi/geometry/operation/sweep.rb +51 -8
- data/lib/sevgi/geometry/operation.rb +62 -0
- data/lib/sevgi/geometry/point.rb +3 -10
- data/lib/sevgi/geometry/segment.rb +3 -10
- data/lib/sevgi/geometry/version.rb +1 -1
- metadata +7 -8
- data/lib/sevgi/geometry/elements/circle.rb +0 -1
- data/lib/sevgi/geometry/elements/curve.rb +0 -1
- data/lib/sevgi/geometry/elements/ellipse.rb +0 -1
data/README.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Sevgi Geometry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Small geometry primitives and operations for Sevgi drawings.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install sevgi-geometry
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Require
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "sevgi/geometry"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
rect = Sevgi::Geometry::Rect[3, 5]
|
|
21
|
+
rect.box.width
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Ruby compatibility
|
|
25
|
+
|
|
26
|
+
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
|
|
27
|
+
|
|
28
|
+
## Native prerequisites
|
|
29
|
+
|
|
30
|
+
None beyond Ruby and this gem's Ruby dependencies.
|
|
31
|
+
|
|
32
|
+
## Links
|
|
33
|
+
|
|
34
|
+
- Documentation: https://sevgi.roktas.dev
|
|
35
|
+
- API documentation: https://www.rubydoc.info/gems/sevgi-geometry
|
|
36
|
+
- Source: https://github.com/roktas/sevgi/tree/main/geometry
|
|
37
|
+
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
|
@@ -19,6 +19,8 @@ module Sevgi
|
|
|
19
19
|
# @raise [NoMethodError] until arced elements are implemented
|
|
20
20
|
def self.arced(...) = Arced.build(...)
|
|
21
21
|
|
|
22
|
+
private_class_method :arced
|
|
23
|
+
|
|
22
24
|
# Core API
|
|
23
25
|
|
|
24
26
|
# Returns a copy moved to a point and optional offset.
|
|
@@ -201,6 +203,11 @@ module Sevgi
|
|
|
201
203
|
|
|
202
204
|
private_class_method :define_line_shortcuts, :define_point_shortcuts, :define_shortcuts
|
|
203
205
|
|
|
206
|
+
# Creates a lined element from a geometry-definition block.
|
|
207
|
+
# @yield evaluates point or segment definitions in the new element
|
|
208
|
+
# @yieldreturn [Object] ignored block result
|
|
209
|
+
# @return [void]
|
|
210
|
+
# @raise [Sevgi::Geometry::Error] when the block is absent or defines inconsistent geometry
|
|
204
211
|
def initialize(&block)
|
|
205
212
|
super()
|
|
206
213
|
|
|
@@ -210,6 +217,7 @@ module Sevgi
|
|
|
210
217
|
|
|
211
218
|
@points ||= calculate_points_from_segments
|
|
212
219
|
@segments ||= calculate_segments_from_points
|
|
220
|
+
freeze_geometry!
|
|
213
221
|
|
|
214
222
|
sanitize
|
|
215
223
|
end
|
|
@@ -234,11 +242,11 @@ module Sevgi
|
|
|
234
242
|
approx.draw!(...)
|
|
235
243
|
end
|
|
236
244
|
|
|
237
|
-
# Returns element points.
|
|
245
|
+
# Returns immutable element points.
|
|
238
246
|
# @param approximate [Boolean] true to round points with the current function precision
|
|
239
|
-
# @return [Array<Sevgi::Geometry::Point>]
|
|
247
|
+
# @return [Array<Sevgi::Geometry::Point>] frozen point collection
|
|
240
248
|
def points(approximate = false)
|
|
241
|
-
approximate ? @points.map(&:approx) : @points
|
|
249
|
+
approximate ? @points.map(&:approx).freeze : @points
|
|
242
250
|
end
|
|
243
251
|
|
|
244
252
|
# Returns the first point.
|
|
@@ -247,15 +255,54 @@ module Sevgi
|
|
|
247
255
|
@position ||= points.first
|
|
248
256
|
end
|
|
249
257
|
|
|
250
|
-
# Returns element segments.
|
|
258
|
+
# Returns immutable element segments.
|
|
251
259
|
# @param approximate [Boolean] true to round segments with the current function precision
|
|
252
|
-
# @return [Array<Sevgi::Geometry::Segment>]
|
|
260
|
+
# @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
|
|
253
261
|
def segments(approximate = false)
|
|
254
|
-
approximate ? @segments.map(&:approx) : @segments
|
|
262
|
+
approximate ? @segments.map(&:approx).freeze : @segments
|
|
255
263
|
end
|
|
256
264
|
|
|
257
265
|
# Affinity methods
|
|
258
266
|
|
|
267
|
+
# @!parse
|
|
268
|
+
# # Returns an element reflected across the selected axes.
|
|
269
|
+
# # @param x [Boolean] reflect across the x-axis
|
|
270
|
+
# # @param y [Boolean] reflect across the y-axis
|
|
271
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
272
|
+
# def reflect(x: true, y: true); end
|
|
273
|
+
#
|
|
274
|
+
# # Returns an element rotated around the origin.
|
|
275
|
+
# # @param a [Numeric] clockwise angle in degrees
|
|
276
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
277
|
+
# def rotate(a); end
|
|
278
|
+
#
|
|
279
|
+
# # Returns an element scaled from the origin.
|
|
280
|
+
# # @param sx [Numeric] x scale factor
|
|
281
|
+
# # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
282
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
283
|
+
# def scale(sx, sy = Undefined); end
|
|
284
|
+
#
|
|
285
|
+
# # Returns an element skewed from the origin.
|
|
286
|
+
# # @param ax [Numeric] x-axis skew angle in degrees
|
|
287
|
+
# # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
288
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
289
|
+
# def skew(ax, ay = Undefined); end
|
|
290
|
+
#
|
|
291
|
+
# # Returns an element skewed along x.
|
|
292
|
+
# # @param a [Numeric] skew angle in degrees
|
|
293
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
294
|
+
# def skew_x(a); end
|
|
295
|
+
#
|
|
296
|
+
# # Returns an element skewed along y.
|
|
297
|
+
# # @param a [Numeric] skew angle in degrees
|
|
298
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
299
|
+
# def skew_y(a); end
|
|
300
|
+
#
|
|
301
|
+
# # Returns an element translated by offset.
|
|
302
|
+
# # @param dx [Numeric] x offset
|
|
303
|
+
# # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
304
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
305
|
+
# def translate(dx, dy = Undefined); end
|
|
259
306
|
Geometry::Affinity.instance_methods.each do |transform|
|
|
260
307
|
define_method(transform) do |*args, **kwargs, &block|
|
|
261
308
|
self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
|
|
@@ -264,36 +311,50 @@ module Sevgi
|
|
|
264
311
|
|
|
265
312
|
# Equality methods
|
|
266
313
|
|
|
267
|
-
#
|
|
314
|
+
# Compares element points with optional numeric precision.
|
|
315
|
+
# @param other [Object] object to compare
|
|
316
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
317
|
+
# @return [Boolean]
|
|
318
|
+
def eq?(other, precision: nil)
|
|
319
|
+
other.instance_of?(self.class) &&
|
|
320
|
+
points.size == other.points.size &&
|
|
321
|
+
points.zip(other.points).all? { |left, right| left.eq?(right, precision:) }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Reports strict element equality by class and exact points.
|
|
268
325
|
# @param other [Object] object to compare
|
|
269
326
|
# @return [Boolean]
|
|
270
|
-
def eql?(other) = self.class
|
|
327
|
+
def eql?(other) = other.instance_of?(self.class) && points == other.points
|
|
271
328
|
|
|
272
329
|
# Returns a hash compatible with strict equality.
|
|
273
330
|
# @return [Integer]
|
|
274
|
-
def hash = [self.class, *points
|
|
331
|
+
def hash = [self.class, *points].hash
|
|
275
332
|
|
|
276
333
|
alias == eql?
|
|
277
334
|
|
|
278
335
|
# Interaction methods
|
|
279
336
|
|
|
280
|
-
# Returns boundary equations for all lines.
|
|
281
|
-
# @return [Array<Sevgi::Geometry::Equation::Linear>]
|
|
282
|
-
def equations = @equations ||= lines.map(&:equation)
|
|
337
|
+
# Returns immutable boundary equations for all lines.
|
|
338
|
+
# @return [Array<Sevgi::Geometry::Equation::Linear>] frozen equation collection
|
|
339
|
+
def equations = @equations ||= lines.map(&:equation).freeze
|
|
283
340
|
|
|
284
341
|
# Intersects the element boundary with an equation.
|
|
342
|
+
#
|
|
343
|
+
# Boundary membership is tested on unrounded candidate points. `precision:`
|
|
344
|
+
# only rounds returned coordinates and controls duplicate collapse after
|
|
345
|
+
# membership has been accepted. When `precision` is nil, returned points use
|
|
346
|
+
# the current function precision.
|
|
285
347
|
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
286
|
-
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
348
|
+
# @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
|
|
287
349
|
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
288
350
|
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
289
351
|
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
290
352
|
def intersection(equation, precision: nil)
|
|
291
|
-
equations
|
|
292
|
-
.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
.uniq
|
|
353
|
+
points = equations.flat_map do |candidate|
|
|
354
|
+
equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
points.map { |point| point.approx(precision) }.uniq
|
|
297
358
|
end
|
|
298
359
|
|
|
299
360
|
# Properties
|
|
@@ -318,12 +379,15 @@ module Sevgi
|
|
|
318
379
|
# @return [Sevgi::Geometry::Segment]
|
|
319
380
|
def head = @head ||= segments.first
|
|
320
381
|
|
|
321
|
-
# Returns boundary lines derived from segments and points.
|
|
322
|
-
# @return [Array<Sevgi::Geometry::Line>]
|
|
382
|
+
# Returns immutable boundary lines derived from segments and points.
|
|
383
|
+
# @return [Array<Sevgi::Geometry::Line>] frozen line collection
|
|
323
384
|
def lines
|
|
324
|
-
@lines ||= segments
|
|
325
|
-
|
|
326
|
-
|
|
385
|
+
@lines ||= segments
|
|
386
|
+
.zip(points[...segments.size])
|
|
387
|
+
.map { |segment, position|
|
|
388
|
+
segment.line(position)
|
|
389
|
+
}
|
|
390
|
+
.freeze
|
|
327
391
|
end
|
|
328
392
|
|
|
329
393
|
# Returns the sum of segment lengths.
|
|
@@ -337,12 +401,17 @@ module Sevgi
|
|
|
337
401
|
# Relation methods
|
|
338
402
|
|
|
339
403
|
# Reports whether a point is inside or on the boundary.
|
|
404
|
+
#
|
|
405
|
+
# Open paths have no filled interior; for them this predicate is true
|
|
406
|
+
# only for points on the actual path boundary.
|
|
340
407
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
341
408
|
# @return [Boolean]
|
|
342
409
|
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
343
410
|
def inside?(point)
|
|
344
411
|
point = Tuple[Point, point]
|
|
345
412
|
|
|
413
|
+
return on?(point) if self.class.open?
|
|
414
|
+
|
|
346
415
|
on?(point) || pnpoly(points, point)
|
|
347
416
|
end
|
|
348
417
|
|
|
@@ -364,6 +433,12 @@ module Sevgi
|
|
|
364
433
|
|
|
365
434
|
private
|
|
366
435
|
|
|
436
|
+
def boundary_point?(point, precision)
|
|
437
|
+
return on?(point) if precision.nil?
|
|
438
|
+
|
|
439
|
+
F.with_precision(precision) { on?(point) }
|
|
440
|
+
end
|
|
441
|
+
|
|
367
442
|
def calculate_points_from_segments
|
|
368
443
|
Error.("No segments found") unless segments
|
|
369
444
|
|
|
@@ -379,6 +454,11 @@ module Sevgi
|
|
|
379
454
|
points.each_cons(2).map { Segment.(*it) }
|
|
380
455
|
end
|
|
381
456
|
|
|
457
|
+
def freeze_geometry!
|
|
458
|
+
@points = @points.dup.freeze
|
|
459
|
+
@segments = @segments.dup.freeze
|
|
460
|
+
end
|
|
461
|
+
|
|
382
462
|
# rubocop:disable Metrics/MethodLength
|
|
383
463
|
# https://wrfranklin.org/Research/Short_Notes/pnpoly.html
|
|
384
464
|
def pnpoly(vertices, test)
|
|
@@ -418,6 +498,8 @@ module Sevgi
|
|
|
418
498
|
# @api private
|
|
419
499
|
class Arced < self
|
|
420
500
|
end
|
|
501
|
+
|
|
502
|
+
private_constant :Arced
|
|
421
503
|
# rubocop:enable Metrics/ClassLength
|
|
422
504
|
end
|
|
423
505
|
|
|
@@ -427,9 +509,5 @@ module Sevgi
|
|
|
427
509
|
require_relative "elements/polyline"
|
|
428
510
|
require_relative "elements/rect"
|
|
429
511
|
require_relative "elements/triangle"
|
|
430
|
-
|
|
431
|
-
require_relative "elements/circle"
|
|
432
|
-
require_relative "elements/curve"
|
|
433
|
-
require_relative "elements/ellipse"
|
|
434
512
|
end
|
|
435
513
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Sevgi
|
|
6
4
|
module Geometry
|
|
7
5
|
# Generated superclass for Line.
|
|
@@ -36,14 +34,33 @@ module Sevgi
|
|
|
36
34
|
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
37
35
|
def self.from_points(...) = new_by_points(...)
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
# Returns the clockwise line angle in degrees.
|
|
38
|
+
# @return [Float]
|
|
39
|
+
def angle = head.angle
|
|
40
|
+
|
|
41
|
+
# Returns the ending point.
|
|
42
|
+
# @return [Sevgi::Geometry::Point]
|
|
43
|
+
def ending = points.last
|
|
44
|
+
|
|
45
|
+
# Reports whether a point is left of the line equation.
|
|
46
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
49
|
+
def left?(point) = equation.left?(point)
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
# Returns the line segment length.
|
|
52
|
+
# @return [Float]
|
|
53
|
+
def length = head.length
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
# Reports whether a point is right of the line equation.
|
|
56
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
59
|
+
def right?(point) = equation.right?(point)
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
# Returns the starting point.
|
|
62
|
+
# @return [Sevgi::Geometry::Point]
|
|
63
|
+
def starting = points.first
|
|
47
64
|
|
|
48
65
|
# Draws the line into a graphics node.
|
|
49
66
|
# @param node [Object] graphics node receiving the drawing command
|
|
@@ -68,8 +85,6 @@ module Sevgi
|
|
|
68
85
|
private
|
|
69
86
|
|
|
70
87
|
def within_range?(point)
|
|
71
|
-
return false if point.nan?
|
|
72
|
-
|
|
73
88
|
point = point.approx
|
|
74
89
|
points = [starting.approx, ending.approx]
|
|
75
90
|
|
|
@@ -72,10 +72,42 @@ module Sevgi
|
|
|
72
72
|
# @return [Float]
|
|
73
73
|
def width = @width ||= segments[0].length
|
|
74
74
|
|
|
75
|
+
# @!parse
|
|
76
|
+
# # Returns the top-left corner.
|
|
77
|
+
# # @return [Sevgi::Geometry::Point]
|
|
78
|
+
# def top_left; end
|
|
79
|
+
#
|
|
80
|
+
# # Returns the top-right corner.
|
|
81
|
+
# # @return [Sevgi::Geometry::Point]
|
|
82
|
+
# def top_right; end
|
|
83
|
+
#
|
|
84
|
+
# # Returns the bottom-right corner.
|
|
85
|
+
# # @return [Sevgi::Geometry::Point]
|
|
86
|
+
# def bottom_right; end
|
|
87
|
+
#
|
|
88
|
+
# # Returns the bottom-left corner.
|
|
89
|
+
# # @return [Sevgi::Geometry::Point]
|
|
90
|
+
# def bottom_left; end
|
|
75
91
|
%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
|
|
76
92
|
define_method(corner) { points[i] }
|
|
77
93
|
end
|
|
78
94
|
|
|
95
|
+
# @!parse
|
|
96
|
+
# # Returns the top side line.
|
|
97
|
+
# # @return [Sevgi::Geometry::Line]
|
|
98
|
+
# def top; end
|
|
99
|
+
#
|
|
100
|
+
# # Returns the right side line.
|
|
101
|
+
# # @return [Sevgi::Geometry::Line]
|
|
102
|
+
# def right; end
|
|
103
|
+
#
|
|
104
|
+
# # Returns the bottom side line.
|
|
105
|
+
# # @return [Sevgi::Geometry::Line]
|
|
106
|
+
# def bottom; end
|
|
107
|
+
#
|
|
108
|
+
# # Returns the left side line.
|
|
109
|
+
# # @return [Sevgi::Geometry::Line]
|
|
110
|
+
# def left; end
|
|
79
111
|
%i[top right bottom left].each_with_index do |side, i|
|
|
80
112
|
define_method(side) { lines[i] }
|
|
81
113
|
end
|
|
@@ -7,37 +7,41 @@ module Sevgi
|
|
|
7
7
|
TriangleBase = Element.lined(3)
|
|
8
8
|
private_constant :TriangleBase
|
|
9
9
|
|
|
10
|
-
# Closed three-sided element built from two adjacent segments.
|
|
10
|
+
# Closed three-sided element built from two non-collinear adjacent segments.
|
|
11
11
|
class Triangle < TriangleBase
|
|
12
12
|
# Builds a triangle from two adjacent segments.
|
|
13
|
+
#
|
|
14
|
+
# The closing segment is the direct vector from the end of `segment_b`
|
|
15
|
+
# back to `position`. Segment order controls orientation; reversing the
|
|
16
|
+
# inputs returns the corresponding opposite orientation. Zero-length or
|
|
17
|
+
# collinear inputs are rejected using the current numeric precision.
|
|
13
18
|
# @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first segment
|
|
14
19
|
# @param segment_b [Sevgi::Geometry::Segment, Array<Numeric>] second segment
|
|
15
20
|
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
16
21
|
# @return [Sevgi::Geometry::Triangle]
|
|
17
|
-
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
22
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced, or the segments are degenerate
|
|
18
23
|
def self.[](segment_a, segment_b, position: Origin)
|
|
19
24
|
a, b = Tuples[Segment, segment_a, segment_b]
|
|
20
25
|
|
|
26
|
+
validate!(a, b)
|
|
21
27
|
new_by_segments(a, b, closing_segment(a, b), position:)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def self.closing_segment(a, b)
|
|
25
|
-
|
|
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)]
|
|
31
|
+
Segment.(b.ending(a.ending(Origin)), Origin)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
def self.
|
|
33
|
-
b_reverse_angle + F.asin(a.length * F.sin(angle_between) / length)
|
|
34
|
-
end
|
|
34
|
+
def self.cross(a, b) = (a.x * b.y) - (a.y * b.x)
|
|
35
35
|
|
|
36
|
-
def self.
|
|
37
|
-
|
|
36
|
+
def self.validate!(a, b)
|
|
37
|
+
if F.zero?(a.length) ||
|
|
38
|
+
F.zero?(b.length) ||
|
|
39
|
+
F.zero?(cross(a, b))
|
|
40
|
+
Error.("Triangle segments must form a non-degenerate triangle")
|
|
41
|
+
end
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
private_class_method :
|
|
44
|
+
private_class_method :closing_segment, :cross, :validate!
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
end
|
|
@@ -7,21 +7,24 @@ module Sevgi
|
|
|
7
7
|
class Linear < Equation
|
|
8
8
|
# Non-axis-aligned linear equation in `y = slope * x + intercept` form.
|
|
9
9
|
class Diagonal < Linear
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
# Returns the line slope.
|
|
11
|
+
# @return [Float]
|
|
12
|
+
attr_reader :slope
|
|
13
|
+
|
|
14
|
+
# Returns the y-intercept.
|
|
15
|
+
# @return [Float]
|
|
16
|
+
attr_reader :intercept
|
|
15
17
|
|
|
16
18
|
# Creates a diagonal linear equation.
|
|
17
19
|
# @param slope [Numeric] line slope
|
|
18
20
|
# @param intercept [Numeric] y-intercept
|
|
19
21
|
# @return [void]
|
|
22
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
|
|
20
23
|
def initialize(slope:, intercept:)
|
|
21
24
|
super()
|
|
22
25
|
|
|
23
|
-
@slope = slope
|
|
24
|
-
@intercept = intercept
|
|
26
|
+
@slope = Real[:slope, slope]
|
|
27
|
+
@intercept = Real[:intercept, intercept]
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# Returns an equation rounded to precision.
|
|
@@ -116,6 +119,7 @@ module Sevgi
|
|
|
116
119
|
# Creates a horizontal equation.
|
|
117
120
|
# @param c [Numeric] y coordinate
|
|
118
121
|
# @return [void]
|
|
122
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
119
123
|
def initialize(c) = super(slope: 0.0, intercept: c)
|
|
120
124
|
|
|
121
125
|
# Returns an equation rounded to precision.
|
|
@@ -146,10 +150,11 @@ module Sevgi
|
|
|
146
150
|
# Creates a vertical equation.
|
|
147
151
|
# @param c [Numeric] x coordinate
|
|
148
152
|
# @return [void]
|
|
153
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
149
154
|
def initialize(c)
|
|
150
155
|
super()
|
|
151
156
|
|
|
152
|
-
@x = c
|
|
157
|
+
@x = Real[:x, c]
|
|
153
158
|
end
|
|
154
159
|
|
|
155
160
|
# Returns an equation rounded to precision.
|
|
@@ -3,12 +3,16 @@
|
|
|
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.
|
|
15
|
+
# @api private
|
|
12
16
|
class Circle
|
|
13
17
|
# Returns the circle equation.
|
|
14
18
|
# @abstract Circle equation support is not implemented yet.
|
|
@@ -18,5 +22,7 @@ module Sevgi
|
|
|
18
22
|
PanicError.("#{self.class}#equation must be implemented")
|
|
19
23
|
end
|
|
20
24
|
end
|
|
25
|
+
|
|
26
|
+
private_constant :Circle
|
|
21
27
|
end
|
|
22
28
|
end
|
|
@@ -8,16 +8,19 @@ module Sevgi
|
|
|
8
8
|
# @param slope [Numeric] line slope
|
|
9
9
|
# @param intercept [Numeric] y-intercept
|
|
10
10
|
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
11
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
|
|
11
12
|
def self.diagonal(slope:, intercept:) = Linear::Diagonal.new(slope:, intercept:)
|
|
12
13
|
|
|
13
14
|
# Builds a horizontal linear equation.
|
|
14
15
|
# @param const [Numeric] y coordinate
|
|
15
16
|
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
17
|
+
# @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
|
|
16
18
|
def self.horizontal(const) = Linear::Horizontal.new(const)
|
|
17
19
|
|
|
18
20
|
# Builds a vertical linear equation.
|
|
19
21
|
# @param const [Numeric] x coordinate
|
|
20
22
|
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
23
|
+
# @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
|
|
21
24
|
def self.vertical(const) = Linear::Vertical.new(const)
|
|
22
25
|
|
|
23
26
|
# Intersects this equation with another equation.
|
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
+
# Validates numeric inputs that can be represented as finite Floats.
|
|
6
|
+
# @api private
|
|
7
|
+
module Real
|
|
8
|
+
# Coerces a public numeric value to a finite Float.
|
|
9
|
+
# @param field [Symbol, String] coordinate or component name
|
|
10
|
+
# @param value [Object] value to coerce
|
|
11
|
+
# @return [Float] finite float value
|
|
12
|
+
# @raise [Sevgi::Geometry::Error] when value is not a Numeric, cannot be converted to Float, or is not finite
|
|
13
|
+
def self.[](field, value)
|
|
14
|
+
unless value.is_a?(::Numeric)
|
|
15
|
+
Error.("Geometry #{field} must be a finite Numeric: #{value.inspect}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
number = coerce(field, value)
|
|
19
|
+
|
|
20
|
+
unless number.is_a?(::Float) && number.finite?
|
|
21
|
+
Error.("Geometry #{field} must be finite: #{value.inspect}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
number
|
|
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 Numeric: #{value.inspect} (#{e.message})")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_class_method :coerce
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_constant :Real
|
|
37
|
+
|
|
5
38
|
# Coerces array-like geometry inputs into typed tuple objects.
|
|
6
39
|
# @api private
|
|
7
40
|
module Tuple
|
|
@@ -13,8 +46,7 @@ module Sevgi
|
|
|
13
46
|
def self.[](klass, arg)
|
|
14
47
|
case arg
|
|
15
48
|
when ::Array
|
|
16
|
-
Error.("Array must have 2 elements: #{arg}"
|
|
17
|
-
Error.("Must be a numeric array: #{arg}") unless arg.all?(::Numeric)
|
|
49
|
+
Error.("Array must have 2 elements: #{arg.inspect}") unless arg.size == 2
|
|
18
50
|
|
|
19
51
|
klass.send(:new, *arg)
|
|
20
52
|
when klass
|