sevgi-geometry 0.93.1 → 0.94.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 +4 -0
- data/LICENSE +5 -0
- data/README.md +34 -2
- data/lib/sevgi/geometry/element.rb +92 -26
- data/lib/sevgi/geometry/elements/line.rb +24 -7
- 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 +6 -3
- data/lib/sevgi/geometry/equation/quadratic.rb +4 -1
- data/lib/sevgi/geometry/equation.rb +3 -0
- data/lib/sevgi/geometry/internal.rb +22 -2
- data/lib/sevgi/geometry/operation/align.rb +2 -0
- data/lib/sevgi/geometry/operation/sweep.rb +49 -8
- data/lib/sevgi/geometry/operation.rb +62 -0
- data/lib/sevgi/geometry/point.rb +2 -1
- data/lib/sevgi/geometry/segment.rb +2 -1
- data/lib/sevgi/geometry/version.rb +1 -1
- metadata +6 -7
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d61732219fd2a5aec56e466123a5aab75df9e1b25734fe8449a2d4235b8d10b4
|
|
4
|
+
data.tar.gz: a0ba1edbfaefbcc54912f5fe0e24cea5af13ec834fde5e3147c81fe4ae3a80c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfdf1b5a5e128e678225357e9073282dfc8376ed0547f9dadb00bad1d7c60243b748077912a3b218226a079ba5605ab5c5d9a6f7abe0d0541132dcc0aeca2328
|
|
7
|
+
data.tar.gz: 37aa84553b62b5d8316923fdfb258de010cafbed34ea3a464a7e5d3134f746acd18e3bb39985a613b64a731d2195246aab755c545345d812a14d141bb8fd2625
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
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 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.
|
|
@@ -210,6 +212,7 @@ module Sevgi
|
|
|
210
212
|
|
|
211
213
|
@points ||= calculate_points_from_segments
|
|
212
214
|
@segments ||= calculate_segments_from_points
|
|
215
|
+
freeze_geometry!
|
|
213
216
|
|
|
214
217
|
sanitize
|
|
215
218
|
end
|
|
@@ -234,11 +237,11 @@ module Sevgi
|
|
|
234
237
|
approx.draw!(...)
|
|
235
238
|
end
|
|
236
239
|
|
|
237
|
-
# Returns element points.
|
|
240
|
+
# Returns immutable element points.
|
|
238
241
|
# @param approximate [Boolean] true to round points with the current function precision
|
|
239
|
-
# @return [Array<Sevgi::Geometry::Point>]
|
|
242
|
+
# @return [Array<Sevgi::Geometry::Point>] frozen point collection
|
|
240
243
|
def points(approximate = false)
|
|
241
|
-
approximate ? @points.map(&:approx) : @points
|
|
244
|
+
approximate ? @points.map(&:approx).freeze : @points
|
|
242
245
|
end
|
|
243
246
|
|
|
244
247
|
# Returns the first point.
|
|
@@ -247,15 +250,54 @@ module Sevgi
|
|
|
247
250
|
@position ||= points.first
|
|
248
251
|
end
|
|
249
252
|
|
|
250
|
-
# Returns element segments.
|
|
253
|
+
# Returns immutable element segments.
|
|
251
254
|
# @param approximate [Boolean] true to round segments with the current function precision
|
|
252
|
-
# @return [Array<Sevgi::Geometry::Segment>]
|
|
255
|
+
# @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
|
|
253
256
|
def segments(approximate = false)
|
|
254
|
-
approximate ? @segments.map(&:approx) : @segments
|
|
257
|
+
approximate ? @segments.map(&:approx).freeze : @segments
|
|
255
258
|
end
|
|
256
259
|
|
|
257
260
|
# Affinity methods
|
|
258
261
|
|
|
262
|
+
# @!parse
|
|
263
|
+
# # Returns an element reflected across the selected axes.
|
|
264
|
+
# # @param x [Boolean] reflect across the x-axis
|
|
265
|
+
# # @param y [Boolean] reflect across the y-axis
|
|
266
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
267
|
+
# def reflect(x: true, y: true); end
|
|
268
|
+
#
|
|
269
|
+
# # Returns an element rotated around the origin.
|
|
270
|
+
# # @param a [Numeric] clockwise angle in degrees
|
|
271
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
272
|
+
# def rotate(a); end
|
|
273
|
+
#
|
|
274
|
+
# # Returns an element scaled from the origin.
|
|
275
|
+
# # @param sx [Numeric] x scale factor
|
|
276
|
+
# # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
277
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
278
|
+
# def scale(sx, sy = Undefined); end
|
|
279
|
+
#
|
|
280
|
+
# # Returns an element skewed from the origin.
|
|
281
|
+
# # @param ax [Numeric] x-axis skew angle in degrees
|
|
282
|
+
# # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
283
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
284
|
+
# def skew(ax, ay = Undefined); end
|
|
285
|
+
#
|
|
286
|
+
# # Returns an element skewed along x.
|
|
287
|
+
# # @param a [Numeric] skew angle in degrees
|
|
288
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
289
|
+
# def skew_x(a); end
|
|
290
|
+
#
|
|
291
|
+
# # Returns an element skewed along y.
|
|
292
|
+
# # @param a [Numeric] skew angle in degrees
|
|
293
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
294
|
+
# def skew_y(a); end
|
|
295
|
+
#
|
|
296
|
+
# # Returns an element translated by offset.
|
|
297
|
+
# # @param dx [Numeric] x offset
|
|
298
|
+
# # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
299
|
+
# # @return [Sevgi::Geometry::Element::Lined]
|
|
300
|
+
# def translate(dx, dy = Undefined); end
|
|
259
301
|
Geometry::Affinity.instance_methods.each do |transform|
|
|
260
302
|
define_method(transform) do |*args, **kwargs, &block|
|
|
261
303
|
self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
|
|
@@ -264,35 +306,48 @@ module Sevgi
|
|
|
264
306
|
|
|
265
307
|
# Equality methods
|
|
266
308
|
|
|
267
|
-
#
|
|
309
|
+
# Compares element points with optional numeric precision.
|
|
268
310
|
# @param other [Object] object to compare
|
|
311
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
269
312
|
# @return [Boolean]
|
|
270
|
-
def
|
|
313
|
+
def eq?(other, precision: nil)
|
|
314
|
+
other.instance_of?(self.class) &&
|
|
315
|
+
points.size == other.points.size &&
|
|
316
|
+
points.zip(other.points).all? { |left, right| left.eq?(right, precision:) }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Reports strict element equality by class and exact points.
|
|
320
|
+
# @param other [Object] object to compare
|
|
321
|
+
# @return [Boolean]
|
|
322
|
+
def eql?(other) = other.instance_of?(self.class) && points == other.points
|
|
271
323
|
|
|
272
324
|
# Returns a hash compatible with strict equality.
|
|
273
325
|
# @return [Integer]
|
|
274
|
-
def hash = [self.class, *points
|
|
326
|
+
def hash = [self.class, *points].hash
|
|
275
327
|
|
|
276
328
|
alias == eql?
|
|
277
329
|
|
|
278
330
|
# Interaction methods
|
|
279
331
|
|
|
280
|
-
# Returns boundary equations for all lines.
|
|
281
|
-
# @return [Array<Sevgi::Geometry::Equation::Linear>]
|
|
282
|
-
def equations = @equations ||= lines.map(&:equation)
|
|
332
|
+
# Returns immutable boundary equations for all lines.
|
|
333
|
+
# @return [Array<Sevgi::Geometry::Equation::Linear>] frozen equation collection
|
|
334
|
+
def equations = @equations ||= lines.map(&:equation).freeze
|
|
283
335
|
|
|
284
336
|
# Intersects the element boundary with an equation.
|
|
337
|
+
#
|
|
338
|
+
# Boundary membership is tested on unrounded candidate points. `precision:`
|
|
339
|
+
# only rounds returned coordinates and controls duplicate collapse after
|
|
340
|
+
# membership has been accepted. When `precision` is nil, returned points use
|
|
341
|
+
# the current function precision.
|
|
285
342
|
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
286
|
-
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
343
|
+
# @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
|
|
287
344
|
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
288
345
|
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
289
346
|
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
290
347
|
def intersection(equation, precision: nil)
|
|
291
348
|
equations
|
|
292
|
-
.
|
|
293
|
-
|
|
294
|
-
end
|
|
295
|
-
.flatten
|
|
349
|
+
.flat_map { |candidate| equation.intersect(candidate).select { |point| on?(point) } }
|
|
350
|
+
.map { |point| point.approx(precision) }
|
|
296
351
|
.uniq
|
|
297
352
|
end
|
|
298
353
|
|
|
@@ -318,12 +373,15 @@ module Sevgi
|
|
|
318
373
|
# @return [Sevgi::Geometry::Segment]
|
|
319
374
|
def head = @head ||= segments.first
|
|
320
375
|
|
|
321
|
-
# Returns boundary lines derived from segments and points.
|
|
322
|
-
# @return [Array<Sevgi::Geometry::Line>]
|
|
376
|
+
# Returns immutable boundary lines derived from segments and points.
|
|
377
|
+
# @return [Array<Sevgi::Geometry::Line>] frozen line collection
|
|
323
378
|
def lines
|
|
324
|
-
@lines ||= segments
|
|
325
|
-
|
|
326
|
-
|
|
379
|
+
@lines ||= segments
|
|
380
|
+
.zip(points[...segments.size])
|
|
381
|
+
.map { |segment, position|
|
|
382
|
+
segment.line(position)
|
|
383
|
+
}
|
|
384
|
+
.freeze
|
|
327
385
|
end
|
|
328
386
|
|
|
329
387
|
# Returns the sum of segment lengths.
|
|
@@ -337,12 +395,17 @@ module Sevgi
|
|
|
337
395
|
# Relation methods
|
|
338
396
|
|
|
339
397
|
# Reports whether a point is inside or on the boundary.
|
|
398
|
+
#
|
|
399
|
+
# Open paths have no filled interior; for them this predicate is true
|
|
400
|
+
# only for points on the actual path boundary.
|
|
340
401
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
341
402
|
# @return [Boolean]
|
|
342
403
|
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
343
404
|
def inside?(point)
|
|
344
405
|
point = Tuple[Point, point]
|
|
345
406
|
|
|
407
|
+
return on?(point) if self.class.open?
|
|
408
|
+
|
|
346
409
|
on?(point) || pnpoly(points, point)
|
|
347
410
|
end
|
|
348
411
|
|
|
@@ -379,6 +442,11 @@ module Sevgi
|
|
|
379
442
|
points.each_cons(2).map { Segment.(*it) }
|
|
380
443
|
end
|
|
381
444
|
|
|
445
|
+
def freeze_geometry!
|
|
446
|
+
@points = @points.dup.freeze
|
|
447
|
+
@segments = @segments.dup.freeze
|
|
448
|
+
end
|
|
449
|
+
|
|
382
450
|
# rubocop:disable Metrics/MethodLength
|
|
383
451
|
# https://wrfranklin.org/Research/Short_Notes/pnpoly.html
|
|
384
452
|
def pnpoly(vertices, test)
|
|
@@ -418,6 +486,8 @@ module Sevgi
|
|
|
418
486
|
# @api private
|
|
419
487
|
class Arced < self
|
|
420
488
|
end
|
|
489
|
+
|
|
490
|
+
private_constant :Arced
|
|
421
491
|
# rubocop:enable Metrics/ClassLength
|
|
422
492
|
end
|
|
423
493
|
|
|
@@ -427,9 +497,5 @@ module Sevgi
|
|
|
427
497
|
require_relative "elements/polyline"
|
|
428
498
|
require_relative "elements/rect"
|
|
429
499
|
require_relative "elements/triangle"
|
|
430
|
-
|
|
431
|
-
require_relative "elements/circle"
|
|
432
|
-
require_relative "elements/curve"
|
|
433
|
-
require_relative "elements/ellipse"
|
|
434
500
|
end
|
|
435
501
|
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
|
|
40
44
|
|
|
41
|
-
|
|
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)
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
# Returns the line segment length.
|
|
52
|
+
# @return [Float]
|
|
53
|
+
def length = head.length
|
|
54
|
+
|
|
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
|
|
@@ -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
|
|
@@ -17,11 +17,12 @@ module Sevgi
|
|
|
17
17
|
# @param slope [Numeric] line slope
|
|
18
18
|
# @param intercept [Numeric] y-intercept
|
|
19
19
|
# @return [void]
|
|
20
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
|
|
20
21
|
def initialize(slope:, intercept:)
|
|
21
22
|
super()
|
|
22
23
|
|
|
23
|
-
@slope = slope
|
|
24
|
-
@intercept = intercept
|
|
24
|
+
@slope = Real[:slope, slope]
|
|
25
|
+
@intercept = Real[:intercept, intercept]
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Returns an equation rounded to precision.
|
|
@@ -116,6 +117,7 @@ module Sevgi
|
|
|
116
117
|
# Creates a horizontal equation.
|
|
117
118
|
# @param c [Numeric] y coordinate
|
|
118
119
|
# @return [void]
|
|
120
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
119
121
|
def initialize(c) = super(slope: 0.0, intercept: c)
|
|
120
122
|
|
|
121
123
|
# Returns an equation rounded to precision.
|
|
@@ -146,10 +148,11 @@ module Sevgi
|
|
|
146
148
|
# Creates a vertical equation.
|
|
147
149
|
# @param c [Numeric] x coordinate
|
|
148
150
|
# @return [void]
|
|
151
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
149
152
|
def initialize(c)
|
|
150
153
|
super()
|
|
151
154
|
|
|
152
|
-
@x = c
|
|
155
|
+
@x = Real[:x, c]
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
# Returns an equation rounded to precision.
|
|
@@ -8,7 +8,8 @@ module Sevgi
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Reserved circle element support.
|
|
12
|
+
# @api private
|
|
12
13
|
class Circle
|
|
13
14
|
# Returns the circle equation.
|
|
14
15
|
# @abstract Circle equation support is not implemented yet.
|
|
@@ -18,5 +19,7 @@ module Sevgi
|
|
|
18
19
|
PanicError.("#{self.class}#equation must be implemented")
|
|
19
20
|
end
|
|
20
21
|
end
|
|
22
|
+
|
|
23
|
+
private_constant :Circle
|
|
21
24
|
end
|
|
22
25
|
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,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
+
# Validates finite real numeric inputs for geometry primitives.
|
|
6
|
+
# @api private
|
|
7
|
+
module Real
|
|
8
|
+
# Coerces a public numeric value to 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 finite Numeric
|
|
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
|
+
value.to_f.tap do |number|
|
|
19
|
+
Error.("Geometry #{field} must be finite: #{value.inspect}") unless number.finite?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private_constant :Real
|
|
25
|
+
|
|
5
26
|
# Coerces array-like geometry inputs into typed tuple objects.
|
|
6
27
|
# @api private
|
|
7
28
|
module Tuple
|
|
@@ -13,8 +34,7 @@ module Sevgi
|
|
|
13
34
|
def self.[](klass, arg)
|
|
14
35
|
case arg
|
|
15
36
|
when ::Array
|
|
16
|
-
Error.("Array must have 2 elements: #{arg}"
|
|
17
|
-
Error.("Must be a numeric array: #{arg}") unless arg.all?(::Numeric)
|
|
37
|
+
Error.("Array must have 2 elements: #{arg.inspect}") unless arg.size == 2
|
|
18
38
|
|
|
19
39
|
klass.send(:new, *arg)
|
|
20
40
|
when klass
|
|
@@ -10,8 +10,11 @@ module Sevgi
|
|
|
10
10
|
# Default maximum number of sweep iterations.
|
|
11
11
|
LIMIT = 1_000
|
|
12
12
|
|
|
13
|
-
# Sweeps parallel lines across
|
|
14
|
-
#
|
|
13
|
+
# Sweeps parallel lines across a lined element in both directions.
|
|
14
|
+
#
|
|
15
|
+
# Generated lines are boundary-to-boundary interior spans. A single
|
|
16
|
+
# sweep position can produce multiple lines for closed concave elements.
|
|
17
|
+
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
15
18
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
16
19
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
17
20
|
# @param step [Numeric] signed distance between sweep lines
|
|
@@ -33,8 +36,11 @@ module Sevgi
|
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
# Sweeps parallel lines and requires at least one result.
|
|
37
|
-
#
|
|
39
|
+
# Sweeps parallel lines across a lined element and requires at least one result.
|
|
40
|
+
#
|
|
41
|
+
# Generated lines are boundary-to-boundary interior spans. A single
|
|
42
|
+
# sweep position can produce multiple lines for closed concave elements.
|
|
43
|
+
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
38
44
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
39
45
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
40
46
|
# @param step [Numeric] signed distance between sweep lines
|
|
@@ -56,7 +62,10 @@ module Sevgi
|
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
# Sweeps parallel lines in one signed direction from an equation.
|
|
59
|
-
#
|
|
65
|
+
#
|
|
66
|
+
# Generated lines are boundary-to-boundary interior spans. A single
|
|
67
|
+
# sweep position can produce multiple lines for closed concave elements.
|
|
68
|
+
# @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
60
69
|
# @param equation [Sevgi::Geometry::Equation] initial sweep equation
|
|
61
70
|
# @param step [Numeric] signed distance between sweep lines
|
|
62
71
|
# @param limit [Integer] maximum iterations
|
|
@@ -69,9 +78,7 @@ module Sevgi
|
|
|
69
78
|
points = element.intersection(equation)
|
|
70
79
|
return lines if points.empty?
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
lines << line
|
|
74
|
-
end
|
|
81
|
+
lines.concat(interior_lines(element, equation, points))
|
|
75
82
|
|
|
76
83
|
equation = equation.shift(step)
|
|
77
84
|
end
|
|
@@ -86,9 +93,43 @@ module Sevgi
|
|
|
86
93
|
def applicable?(element)
|
|
87
94
|
element.respond_to?(:intersection)
|
|
88
95
|
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def interior_lines(element, equation, points)
|
|
100
|
+
if points.size == 2
|
|
101
|
+
line = simple_line(points)
|
|
102
|
+
|
|
103
|
+
return line ? [line] : []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sorted_points(equation, points).each_cons(2).filter_map do |starting, ending|
|
|
107
|
+
next unless element.inside?(midpoint(starting, ending))
|
|
108
|
+
|
|
109
|
+
simple_line([starting, ending])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def midpoint(starting, ending)
|
|
114
|
+
Point[(starting.x + ending.x) / 2.0, (starting.y + ending.y) / 2.0]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def simple_line(points)
|
|
118
|
+
line = Line.(*points)
|
|
119
|
+
|
|
120
|
+
line unless line.ignorable?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sorted_points(equation, points)
|
|
124
|
+
points.sort_by do |point|
|
|
125
|
+
equation.is_a?(Equation::Linear::Vertical) ? [point.y, point.x] : [point.x, point.y]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
89
128
|
end
|
|
90
129
|
|
|
91
130
|
register(Sweep, :sweep, :sweep!, :unisweep)
|
|
131
|
+
|
|
132
|
+
private_constant :Sweep
|
|
92
133
|
end
|
|
93
134
|
end
|
|
94
135
|
end
|
|
@@ -12,6 +12,66 @@ module Sevgi
|
|
|
12
12
|
# Raised when an operation does not apply to the target element.
|
|
13
13
|
OperationInapplicableError = Class.new(Error)
|
|
14
14
|
|
|
15
|
+
# @!parse
|
|
16
|
+
# class << self
|
|
17
|
+
# # Returns an element translated to align with another element.
|
|
18
|
+
# # @param element [Sevgi::Geometry::Element] element to move
|
|
19
|
+
# # @param other [Sevgi::Geometry::Element] reference element
|
|
20
|
+
# # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
|
|
21
|
+
# # @return [Sevgi::Geometry::Element] translated element
|
|
22
|
+
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
|
|
23
|
+
# # @raise [Sevgi::ArgumentError] when alignment is unknown
|
|
24
|
+
# def align(element, other, alignment = :center); end
|
|
25
|
+
#
|
|
26
|
+
# # Returns the offset needed to align one element with another.
|
|
27
|
+
# # @param element [Sevgi::Geometry::Element] element to move
|
|
28
|
+
# # @param other [Sevgi::Geometry::Element] reference element
|
|
29
|
+
# # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
|
|
30
|
+
# # @return [Sevgi::Geometry::Point] translation offset
|
|
31
|
+
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
|
|
32
|
+
# # @raise [Sevgi::ArgumentError] when alignment is unknown
|
|
33
|
+
# def alignment(element, other, alignment = :center); end
|
|
34
|
+
#
|
|
35
|
+
# # Sweeps parallel lines across a lined element in both directions.
|
|
36
|
+
# # @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
37
|
+
# # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
38
|
+
# # @param angle [Numeric] clockwise sweep line angle in degrees
|
|
39
|
+
# # @param step [Numeric] signed distance between sweep lines
|
|
40
|
+
# # @param limit [Integer] maximum iterations per direction
|
|
41
|
+
# # @yield [lines] optional hook receiving the generated lines
|
|
42
|
+
# # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
43
|
+
# # @yieldreturn [void]
|
|
44
|
+
# # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
45
|
+
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
46
|
+
# # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
|
|
47
|
+
# # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
48
|
+
# def sweep(element, initial:, angle:, step:, limit: Sweep::LIMIT); end
|
|
49
|
+
#
|
|
50
|
+
# # Sweeps parallel lines across a lined element and requires at least one result.
|
|
51
|
+
# # @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
52
|
+
# # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
53
|
+
# # @param angle [Numeric] clockwise sweep line angle in degrees
|
|
54
|
+
# # @param step [Numeric] signed distance between sweep lines
|
|
55
|
+
# # @param limit [Integer] maximum iterations per direction
|
|
56
|
+
# # @yield [lines] optional hook receiving the generated lines
|
|
57
|
+
# # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
58
|
+
# # @yieldreturn [void]
|
|
59
|
+
# # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
60
|
+
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
61
|
+
# # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
|
|
62
|
+
# # @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
|
|
63
|
+
# def sweep!(element, initial:, angle:, step:, limit: Sweep::LIMIT); end
|
|
64
|
+
#
|
|
65
|
+
# # Sweeps parallel lines in one signed direction from an equation.
|
|
66
|
+
# # @param element [Sevgi::Geometry::Element::Lined] element to intersect
|
|
67
|
+
# # @param equation [Sevgi::Geometry::Equation] initial sweep equation
|
|
68
|
+
# # @param step [Numeric] signed distance between sweep lines
|
|
69
|
+
# # @param limit [Integer] maximum iterations
|
|
70
|
+
# # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
71
|
+
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
72
|
+
# # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
73
|
+
# def unisweep(element, equation, step, limit: Sweep::LIMIT); end
|
|
74
|
+
# end
|
|
15
75
|
# Registers one or more public operation methods.
|
|
16
76
|
# @api private
|
|
17
77
|
# @param handler [Module] operation handler module
|
|
@@ -19,6 +79,8 @@ module Sevgi
|
|
|
19
79
|
# @return [Array<Symbol>] registered operation names
|
|
20
80
|
def register(handler, *operations) = operations.each { |operation| def_operation(operation, handler) }
|
|
21
81
|
|
|
82
|
+
private :register
|
|
83
|
+
|
|
22
84
|
private
|
|
23
85
|
|
|
24
86
|
def def_operation(operation, handler)
|
data/lib/sevgi/geometry/point.rb
CHANGED
|
@@ -100,7 +100,8 @@ module Sevgi
|
|
|
100
100
|
# @param x [Numeric] x coordinate
|
|
101
101
|
# @param y [Numeric] y coordinate
|
|
102
102
|
# @return [void]
|
|
103
|
-
|
|
103
|
+
# @raise [Sevgi::Geometry::Error] when a coordinate is not a finite Numeric
|
|
104
|
+
def initialize(x:, y:) = super(x: Real[:x, x], y: Real[:y, y])
|
|
104
105
|
|
|
105
106
|
# Compares points by x, then y.
|
|
106
107
|
# @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
|
|
@@ -66,7 +66,8 @@ module Sevgi
|
|
|
66
66
|
# @param length [Numeric] segment length
|
|
67
67
|
# @param angle [Numeric] clockwise angle in degrees
|
|
68
68
|
# @return [void]
|
|
69
|
-
|
|
69
|
+
# @raise [Sevgi::Geometry::Error] when a component is not a finite Numeric
|
|
70
|
+
def initialize(length:, angle:) = super(length: Real[:length, length], angle: Real[:angle, angle])
|
|
70
71
|
|
|
71
72
|
# Compares segments by length.
|
|
72
73
|
# @param other [Sevgi::Geometry::Segment, Array<Numeric>] segment to compare
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-geometry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.94.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -15,26 +15,25 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.94.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.94.0
|
|
26
26
|
description: Enhances the Sevgi toolkit with geometry objects and methods.
|
|
27
27
|
email: roktas@gmail.com
|
|
28
28
|
executables: []
|
|
29
29
|
extensions: []
|
|
30
30
|
extra_rdoc_files: []
|
|
31
31
|
files:
|
|
32
|
+
- CHANGELOG.md
|
|
33
|
+
- LICENSE
|
|
32
34
|
- README.md
|
|
33
35
|
- lib/sevgi/geometry.rb
|
|
34
36
|
- lib/sevgi/geometry/element.rb
|
|
35
|
-
- lib/sevgi/geometry/elements/circle.rb
|
|
36
|
-
- lib/sevgi/geometry/elements/curve.rb
|
|
37
|
-
- lib/sevgi/geometry/elements/ellipse.rb
|
|
38
37
|
- lib/sevgi/geometry/elements/line.rb
|
|
39
38
|
- lib/sevgi/geometry/elements/parallelogram.rb
|
|
40
39
|
- lib/sevgi/geometry/elements/polygon.rb
|
|
@@ -67,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
67
66
|
requirements:
|
|
68
67
|
- - ">="
|
|
69
68
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: 3.4.0
|
|
69
|
+
version: 3.4.0
|
|
71
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
71
|
requirements:
|
|
73
72
|
- - ">="
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|