sevgi-geometry 0.73.2 → 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 +37 -0
- data/lib/sevgi/geometry/element.rb +292 -46
- data/lib/sevgi/geometry/elements/line.rb +71 -15
- 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 +88 -8
- data/lib/sevgi/geometry/elements/triangle.rb +47 -0
- data/lib/sevgi/geometry/equation/linear.rb +152 -9
- data/lib/sevgi/geometry/equation/quadratic.rb +10 -1
- data/lib/sevgi/geometry/equation.rb +50 -9
- data/lib/sevgi/geometry/errors.rb +1 -0
- data/lib/sevgi/geometry/internal.rb +36 -2
- data/lib/sevgi/geometry/operation/align.rb +37 -9
- data/lib/sevgi/geometry/operation/sweep.rb +86 -8
- data/lib/sevgi/geometry/operation.rb +71 -0
- data/lib/sevgi/geometry/point.rb +106 -8
- data/lib/sevgi/geometry/segment.rb +90 -15
- data/lib/sevgi/geometry/version.rb +2 -1
- data/lib/sevgi/geometry.rb +9 -0
- metadata +8 -9
- 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/lib/sevgi/geometry/elements/parm.rb +0 -37
- data/lib/sevgi/geometry/elements/tri.rb +0 -18
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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Sevgi Geometry
|
|
2
|
+
|
|
3
|
+
Small geometry primitives and operations for Sevgi drawings.
|
|
4
|
+
|
|
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
|
|
@@ -2,47 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
+
# Base class for geometric elements.
|
|
5
6
|
class Element
|
|
7
|
+
# @overload lined(size = Undefined, open: false)
|
|
8
|
+
# Builds a lined element subclass.
|
|
9
|
+
# @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
|
|
10
|
+
# @param open [Boolean] true for an open path, false for a closed path
|
|
11
|
+
# @return [Class] subclass of {Sevgi::Geometry::Element::Lined}
|
|
6
12
|
def self.lined(...) = Lined.build(...)
|
|
7
13
|
|
|
14
|
+
# @overload arced(*args)
|
|
15
|
+
# Builds an arced element subclass.
|
|
16
|
+
# @api private
|
|
17
|
+
# @param args [Array<Object>] arced factory arguments
|
|
18
|
+
# @return [Class]
|
|
19
|
+
# @raise [NoMethodError] until arced elements are implemented
|
|
8
20
|
def self.arced(...) = Arced.build(...)
|
|
9
21
|
|
|
22
|
+
private_class_method :arced
|
|
23
|
+
|
|
10
24
|
# Core API
|
|
11
25
|
|
|
26
|
+
# Returns a copy moved to a point and optional offset.
|
|
27
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>, nil] target position, or nil to keep current position
|
|
28
|
+
# @param dx [Numeric] additional x offset
|
|
29
|
+
# @param dy [Numeric] additional y offset
|
|
30
|
+
# @return [Sevgi::Geometry::Element] translated element
|
|
31
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
12
32
|
def at(point = nil, dx: 0, dy: 0)
|
|
33
|
+
point = point ? Tuple[Point, point] : position
|
|
34
|
+
|
|
13
35
|
translate(
|
|
14
|
-
(
|
|
36
|
+
(point.x - position.x) + dx,
|
|
15
37
|
(point.y - position.y) + dy
|
|
16
38
|
)
|
|
17
39
|
end
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
# Returns the bounding rectangle.
|
|
42
|
+
# @abstract Subclasses implement element-specific bounds.
|
|
43
|
+
# @return [Sevgi::Geometry::Rect]
|
|
44
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement box
|
|
45
|
+
def box = PanicError.("#{self.class}#box must be implemented")
|
|
46
|
+
|
|
47
|
+
# Returns equations that define the element boundary.
|
|
48
|
+
# @abstract Subclasses implement element-specific equations.
|
|
49
|
+
# @return [Array<Sevgi::Geometry::Equation>]
|
|
50
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement equations
|
|
51
|
+
def equations = PanicError.("#{self.class}#equations must be implemented")
|
|
52
|
+
|
|
53
|
+
# Reports whether the element has zero bounding width and height.
|
|
54
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
55
|
+
# @return [Boolean]
|
|
23
56
|
def ignorable?(precision: nil) = F.zero?(box.width, precision:) && F.zero?(box.height, precision:)
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
# Returns the element position.
|
|
59
|
+
# @abstract Subclasses implement element-specific positioning.
|
|
60
|
+
# @return [Sevgi::Geometry::Point]
|
|
61
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement position
|
|
62
|
+
def position = PanicError.("#{self.class}#position must be implemented")
|
|
26
63
|
|
|
27
|
-
|
|
64
|
+
# Returns a translated copy.
|
|
65
|
+
# @abstract Subclasses implement element-specific translation.
|
|
66
|
+
# @param _x [Numeric] x offset
|
|
67
|
+
# @param _y [Numeric] y offset
|
|
68
|
+
# @return [Sevgi::Geometry::Element]
|
|
69
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement translate
|
|
70
|
+
def translate(_x, _y) = PanicError.("#{self.class}#translate must be implemented")
|
|
28
71
|
|
|
29
72
|
# rubocop:disable Metrics/ClassLength
|
|
73
|
+
# Element whose boundary is represented by straight segments.
|
|
30
74
|
class Lined < self
|
|
75
|
+
# Open lined element base class.
|
|
31
76
|
Open = Class.new(self) do
|
|
77
|
+
# Draws the element as an SVG polyline.
|
|
78
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
79
|
+
# @return [Object] graphics node command result
|
|
32
80
|
def draw!(node, **) = node.polyline(points: points.map { it.deconstruct.join(",") }, **)
|
|
33
81
|
end
|
|
34
82
|
|
|
83
|
+
# Closed lined element base class.
|
|
35
84
|
Close = Class.new(self) do
|
|
85
|
+
# Creates a closed element from points, appending the first point.
|
|
86
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
87
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
88
|
+
# @raise [Sevgi::Geometry::Error] when any point cannot be coerced
|
|
36
89
|
def self.new_by_points(*points) = super(*points, points.first)
|
|
37
90
|
|
|
91
|
+
# Draws the element as an SVG polygon.
|
|
92
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
93
|
+
# @return [Object] graphics node command result
|
|
38
94
|
def draw!(node, **) = node.polygon(points: points.map { it.deconstruct.join(",") }, **)
|
|
39
95
|
end
|
|
40
96
|
|
|
41
97
|
# Class methods
|
|
42
98
|
|
|
99
|
+
# Point shortcut names generated for fixed-size lined elements.
|
|
43
100
|
SHORTCUTS = ("A".."Z").to_a.freeze
|
|
44
101
|
|
|
45
|
-
#
|
|
102
|
+
# Builds a concrete lined element class.
|
|
103
|
+
# @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
|
|
104
|
+
# @param open [Boolean] true for an open path, false for a closed path
|
|
105
|
+
# @return [Class] lined element subclass
|
|
46
106
|
def self.build(size = Undefined, open: false)
|
|
47
107
|
Class.new(open ? Open : Close) do
|
|
48
108
|
define_singleton_method(:close?) { !open }
|
|
@@ -53,33 +113,64 @@ module Sevgi
|
|
|
53
113
|
|
|
54
114
|
define_singleton_method(:size) { size }
|
|
55
115
|
|
|
56
|
-
unless size == Undefined
|
|
57
|
-
SHORTCUTS[..size.clamp(..SHORTCUTS.size)].each_with_index do |name, i|
|
|
58
|
-
define_method(name) { points[i] or Error.("No such point: #{name}") }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
methods = SHORTCUTS[..size.clamp(..SHORTCUTS.size)].each_cons(2).with_index.map do |names, i|
|
|
62
|
-
define_method(name = names.join) { lines[i] or Error.("No such line: #{name}") }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
alias_method("#{SHORTCUTS[size - 1]}#{SHORTCUTS.first}", methods.last)
|
|
66
|
-
end
|
|
116
|
+
Lined.send(:define_shortcuts, self, size, open:) unless size == Undefined
|
|
67
117
|
end
|
|
68
|
-
# rubocop:enable Metrics/MethodLength
|
|
69
118
|
end
|
|
70
119
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
# @overload [](*segments, position: Origin)
|
|
121
|
+
# Builds an element from segments.
|
|
122
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
123
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
124
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
125
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
126
|
+
def self.[](...) = from_segments(...)
|
|
127
|
+
|
|
128
|
+
# @overload call(*points)
|
|
129
|
+
# Builds an element from points.
|
|
130
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
131
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
132
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
133
|
+
def self.call(...) = from_points(...)
|
|
134
|
+
|
|
135
|
+
# @overload from_points(*points)
|
|
136
|
+
# Builds an element from points.
|
|
137
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
138
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
139
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
140
|
+
def self.from_points(...) = new_by_points(...)
|
|
141
|
+
|
|
142
|
+
# @overload from_segments(*segments, position: Origin)
|
|
143
|
+
# Builds an element from segments.
|
|
144
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
145
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
146
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
147
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
148
|
+
def self.from_segments(...) = new_by_segments(...)
|
|
149
|
+
|
|
150
|
+
# @overload new_by_points(*points)
|
|
151
|
+
# Builds an element from points, applying closed-path behavior where appropriate.
|
|
152
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
153
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
154
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
75
155
|
def self.new_by_points(...) = new_by_points!(...)
|
|
76
156
|
|
|
157
|
+
# Builds an element from an exact point path.
|
|
158
|
+
#
|
|
159
|
+
# Closed classes require the closing point to be supplied by the caller.
|
|
160
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] exact boundary points
|
|
161
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
162
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced or do not satisfy the class path contract
|
|
77
163
|
def self.new_by_points!(*points)
|
|
78
164
|
new do
|
|
79
165
|
@points = Tuples[Point, *points]
|
|
80
166
|
end
|
|
81
167
|
end
|
|
82
168
|
|
|
169
|
+
# Builds an element from segments and a start position.
|
|
170
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
171
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
172
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
173
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
83
174
|
def self.new_by_segments(*segments, position: Origin)
|
|
84
175
|
new do
|
|
85
176
|
@position = Tuple[Point, position]
|
|
@@ -89,6 +180,29 @@ module Sevgi
|
|
|
89
180
|
|
|
90
181
|
private_class_method(:new)
|
|
91
182
|
|
|
183
|
+
def self.define_line_shortcuts(klass, point_names, open:)
|
|
184
|
+
line_names = point_names.each_cons(2).map(&:join)
|
|
185
|
+
line_names << "#{point_names.last}#{point_names.first}" if !open && point_names.any?
|
|
186
|
+
|
|
187
|
+
line_names.each_with_index do |name, i|
|
|
188
|
+
klass.define_method(name) { lines[i] or Error.("No such line: #{name}") }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.define_point_shortcuts(klass, point_names)
|
|
193
|
+
point_names.each_with_index do |name, i|
|
|
194
|
+
klass.define_method(name) { points[i] or Error.("No such point: #{name}") }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.define_shortcuts(klass, size, open:)
|
|
199
|
+
point_names = SHORTCUTS.first([open ? size + 1 : size, SHORTCUTS.size].min)
|
|
200
|
+
define_point_shortcuts(klass, point_names)
|
|
201
|
+
define_line_shortcuts(klass, point_names, open:)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private_class_method :define_line_shortcuts, :define_point_shortcuts, :define_shortcuts
|
|
205
|
+
|
|
92
206
|
def initialize(&block)
|
|
93
207
|
super()
|
|
94
208
|
|
|
@@ -98,12 +212,15 @@ module Sevgi
|
|
|
98
212
|
|
|
99
213
|
@points ||= calculate_points_from_segments
|
|
100
214
|
@segments ||= calculate_segments_from_points
|
|
215
|
+
freeze_geometry!
|
|
101
216
|
|
|
102
217
|
sanitize
|
|
103
218
|
end
|
|
104
219
|
|
|
105
220
|
# Core methods
|
|
106
221
|
|
|
222
|
+
# Returns an element with approximate points and segments.
|
|
223
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
107
224
|
def approx
|
|
108
225
|
points, segments = points(true), segments(true)
|
|
109
226
|
self.class.send(:new) do
|
|
@@ -111,24 +228,76 @@ module Sevgi
|
|
|
111
228
|
end
|
|
112
229
|
end
|
|
113
230
|
|
|
231
|
+
# @overload draw(node, **attributes)
|
|
232
|
+
# Draws an approximate element into a graphics node.
|
|
233
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
234
|
+
# @param attributes [Hash] drawing attributes
|
|
235
|
+
# @return [Object] graphics node command result
|
|
114
236
|
def draw(...)
|
|
115
237
|
approx.draw!(...)
|
|
116
238
|
end
|
|
117
239
|
|
|
240
|
+
# Returns immutable element points.
|
|
241
|
+
# @param approximate [Boolean] true to round points with the current function precision
|
|
242
|
+
# @return [Array<Sevgi::Geometry::Point>] frozen point collection
|
|
118
243
|
def points(approximate = false)
|
|
119
|
-
approximate ?
|
|
244
|
+
approximate ? @points.map(&:approx).freeze : @points
|
|
120
245
|
end
|
|
121
246
|
|
|
247
|
+
# Returns the first point.
|
|
248
|
+
# @return [Sevgi::Geometry::Point]
|
|
122
249
|
def position
|
|
123
250
|
@position ||= points.first
|
|
124
251
|
end
|
|
125
252
|
|
|
253
|
+
# Returns immutable element segments.
|
|
254
|
+
# @param approximate [Boolean] true to round segments with the current function precision
|
|
255
|
+
# @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
|
|
126
256
|
def segments(approximate = false)
|
|
127
|
-
approximate ?
|
|
257
|
+
approximate ? @segments.map(&:approx).freeze : @segments
|
|
128
258
|
end
|
|
129
259
|
|
|
130
260
|
# Affinity methods
|
|
131
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
|
|
132
301
|
Geometry::Affinity.instance_methods.each do |transform|
|
|
133
302
|
define_method(transform) do |*args, **kwargs, &block|
|
|
134
303
|
self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
|
|
@@ -137,51 +306,123 @@ module Sevgi
|
|
|
137
306
|
|
|
138
307
|
# Equality methods
|
|
139
308
|
|
|
140
|
-
|
|
309
|
+
# Compares element points with optional numeric precision.
|
|
310
|
+
# @param other [Object] object to compare
|
|
311
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
312
|
+
# @return [Boolean]
|
|
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
|
|
141
323
|
|
|
142
|
-
|
|
324
|
+
# Returns a hash compatible with strict equality.
|
|
325
|
+
# @return [Integer]
|
|
326
|
+
def hash = [self.class, *points].hash
|
|
143
327
|
|
|
144
328
|
alias == eql?
|
|
145
329
|
|
|
146
330
|
# Interaction methods
|
|
147
331
|
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
335
|
+
|
|
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.
|
|
342
|
+
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
343
|
+
# @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
|
|
344
|
+
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
345
|
+
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
346
|
+
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
150
347
|
def intersection(equation, precision: nil)
|
|
151
348
|
equations
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
end
|
|
155
|
-
.flatten
|
|
349
|
+
.flat_map { |candidate| equation.intersect(candidate).select { |point| on?(point) } }
|
|
350
|
+
.map { |point| point.approx(precision) }
|
|
156
351
|
.uniq
|
|
157
352
|
end
|
|
158
353
|
|
|
159
354
|
# Properties
|
|
160
355
|
|
|
356
|
+
# Returns a line by index.
|
|
357
|
+
# @param i [Integer] line index
|
|
358
|
+
# @return [Sevgi::Geometry::Line]
|
|
359
|
+
# @raise [Sevgi::Geometry::Error] when no line exists for index
|
|
161
360
|
def [](i) = lines[i].tap { |line| Error.("No line exist for index: #{i}") unless line }
|
|
162
361
|
|
|
163
|
-
|
|
362
|
+
# Returns the bounding rectangle.
|
|
363
|
+
# @return [Sevgi::Geometry::Rect]
|
|
364
|
+
def box = Rect.from_corners([(xs = points.map(&:x)).min, (ys = points.map(&:y)).min], [xs.max, ys.max])
|
|
164
365
|
|
|
366
|
+
# Returns a point by index.
|
|
367
|
+
# @param i [Integer] point index
|
|
368
|
+
# @return [Sevgi::Geometry::Point]
|
|
369
|
+
# @raise [Sevgi::Geometry::Error] when no point exists for index
|
|
165
370
|
def call(i) = points[i].tap { Error.("No point exist for index: #{i}") unless it }
|
|
166
371
|
|
|
372
|
+
# Returns the first segment.
|
|
373
|
+
# @return [Sevgi::Geometry::Segment]
|
|
167
374
|
def head = @head ||= segments.first
|
|
168
375
|
|
|
376
|
+
# Returns immutable boundary lines derived from segments and points.
|
|
377
|
+
# @return [Array<Sevgi::Geometry::Line>] frozen line collection
|
|
169
378
|
def lines
|
|
170
|
-
@lines ||= segments
|
|
171
|
-
|
|
172
|
-
|
|
379
|
+
@lines ||= segments
|
|
380
|
+
.zip(points[...segments.size])
|
|
381
|
+
.map { |segment, position|
|
|
382
|
+
segment.line(position)
|
|
383
|
+
}
|
|
384
|
+
.freeze
|
|
173
385
|
end
|
|
174
386
|
|
|
387
|
+
# Returns the sum of segment lengths.
|
|
388
|
+
# @return [Float]
|
|
175
389
|
def perimeter = @perimeter ||= segments.sum(&:length)
|
|
176
390
|
|
|
391
|
+
# Returns the last segment.
|
|
392
|
+
# @return [Sevgi::Geometry::Segment]
|
|
177
393
|
def tail = @tail ||= segments.last
|
|
178
394
|
|
|
179
395
|
# Relation methods
|
|
180
396
|
|
|
181
|
-
|
|
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.
|
|
401
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
402
|
+
# @return [Boolean]
|
|
403
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
404
|
+
def inside?(point)
|
|
405
|
+
point = Tuple[Point, point]
|
|
406
|
+
|
|
407
|
+
return on?(point) if self.class.open?
|
|
182
408
|
|
|
183
|
-
|
|
409
|
+
on?(point) || pnpoly(points, point)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Reports whether a point is on the boundary.
|
|
413
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
414
|
+
# @return [Boolean]
|
|
415
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
416
|
+
def on?(point)
|
|
417
|
+
point = Tuple[Point, point]
|
|
418
|
+
|
|
419
|
+
lines.any? { it.over?(point) }
|
|
420
|
+
end
|
|
184
421
|
|
|
422
|
+
# Reports whether a point is outside the element boundary.
|
|
423
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
424
|
+
# @return [Boolean]
|
|
425
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
185
426
|
def outside?(point) = !inside?(point)
|
|
186
427
|
|
|
187
428
|
private
|
|
@@ -201,6 +442,11 @@ module Sevgi
|
|
|
201
442
|
points.each_cons(2).map { Segment.(*it) }
|
|
202
443
|
end
|
|
203
444
|
|
|
445
|
+
def freeze_geometry!
|
|
446
|
+
@points = @points.dup.freeze
|
|
447
|
+
@segments = @segments.dup.freeze
|
|
448
|
+
end
|
|
449
|
+
|
|
204
450
|
# rubocop:disable Metrics/MethodLength
|
|
205
451
|
# https://wrfranklin.org/Research/Short_Notes/pnpoly.html
|
|
206
452
|
def pnpoly(vertices, test)
|
|
@@ -236,20 +482,20 @@ module Sevgi
|
|
|
236
482
|
end
|
|
237
483
|
end
|
|
238
484
|
|
|
485
|
+
# Reserved base for future arced elements.
|
|
486
|
+
# @api private
|
|
239
487
|
class Arced < self
|
|
240
488
|
end
|
|
489
|
+
|
|
490
|
+
private_constant :Arced
|
|
241
491
|
# rubocop:enable Metrics/ClassLength
|
|
242
492
|
end
|
|
243
493
|
|
|
244
494
|
require_relative "elements/line"
|
|
245
|
-
require_relative "elements/
|
|
495
|
+
require_relative "elements/parallelogram"
|
|
246
496
|
require_relative "elements/polygon"
|
|
247
497
|
require_relative "elements/polyline"
|
|
248
498
|
require_relative "elements/rect"
|
|
249
|
-
require_relative "elements/
|
|
250
|
-
|
|
251
|
-
require_relative "elements/circle"
|
|
252
|
-
require_relative "elements/curve"
|
|
253
|
-
require_relative "elements/ellipse"
|
|
499
|
+
require_relative "elements/triangle"
|
|
254
500
|
end
|
|
255
501
|
end
|
|
@@ -1,30 +1,86 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Sevgi
|
|
6
4
|
module Geometry
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
# Generated superclass for Line.
|
|
6
|
+
# @api private
|
|
7
|
+
LineBase = Element.lined(1, open: true)
|
|
8
|
+
private_constant :LineBase
|
|
9
|
+
|
|
10
|
+
# Open lined element with one segment.
|
|
11
|
+
class Line < LineBase
|
|
12
|
+
# @overload [](length, angle, position: Origin)
|
|
13
|
+
# Builds a line from length and angle.
|
|
14
|
+
# @param length [Numeric] line length
|
|
15
|
+
# @param angle [Numeric] clockwise angle in degrees
|
|
16
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
17
|
+
# @return [Sevgi::Geometry::Line]
|
|
18
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
19
|
+
def self.[](...) = from_length_angle(...)
|
|
20
|
+
|
|
21
|
+
# Builds a line from length and angle.
|
|
22
|
+
# @param length [Numeric] line length
|
|
23
|
+
# @param angle [Numeric] clockwise angle in degrees
|
|
24
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
25
|
+
# @return [Sevgi::Geometry::Line]
|
|
26
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
27
|
+
def self.from_length_angle(length, angle, position: Origin) = new_by_segments(Segment[length, angle], position:)
|
|
28
|
+
|
|
29
|
+
# @overload from_points(starting, ending)
|
|
30
|
+
# Builds a line from two endpoints.
|
|
31
|
+
# @param starting [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
32
|
+
# @param ending [Sevgi::Geometry::Point, Array<Numeric>] ending point
|
|
33
|
+
# @return [Sevgi::Geometry::Line]
|
|
34
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
35
|
+
def self.from_points(...) = new_by_points(...)
|
|
9
36
|
|
|
10
|
-
|
|
37
|
+
# Returns the clockwise line angle in degrees.
|
|
38
|
+
# @return [Float]
|
|
39
|
+
def angle = head.angle
|
|
11
40
|
|
|
12
|
-
|
|
41
|
+
# Returns the ending point.
|
|
42
|
+
# @return [Sevgi::Geometry::Point]
|
|
43
|
+
def ending = points.last
|
|
13
44
|
|
|
14
|
-
|
|
15
|
-
|
|
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)
|
|
16
50
|
|
|
17
|
-
|
|
51
|
+
# Returns the line segment length.
|
|
52
|
+
# @return [Float]
|
|
53
|
+
def length = head.length
|
|
18
54
|
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
21
60
|
|
|
22
|
-
|
|
61
|
+
# Returns the starting point.
|
|
62
|
+
# @return [Sevgi::Geometry::Point]
|
|
63
|
+
def starting = points.first
|
|
23
64
|
|
|
24
|
-
#
|
|
25
|
-
|
|
65
|
+
# Draws the line into a graphics node.
|
|
66
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
67
|
+
# @return [Object] graphics node command result
|
|
68
|
+
def draw!(node, **) = node.LineTo(x1: position.x, y1: position.y, x2: ending.x, y2: ending.y, **)
|
|
69
|
+
|
|
70
|
+
# Reports whether a point lies on the finite line segment.
|
|
71
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
74
|
+
def over?(point)
|
|
75
|
+
point = Tuple[Point, point]
|
|
76
|
+
|
|
77
|
+
within_range?(point) && equation.on?(point)
|
|
78
|
+
end
|
|
26
79
|
|
|
27
|
-
|
|
80
|
+
# Returns a parallel line shifted by a signed perpendicular offset.
|
|
81
|
+
# @param distance [Numeric] signed perpendicular offset
|
|
82
|
+
# @return [Sevgi::Geometry::Line]
|
|
83
|
+
def shift(distance) = translate(distance * F.sin(angle), -distance * F.cos(angle))
|
|
28
84
|
|
|
29
85
|
private
|
|
30
86
|
|