sevgi-geometry 0.73.2 → 0.93.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/sevgi/geometry/element.rb +211 -31
- data/lib/sevgi/geometry/elements/line.rb +47 -8
- data/lib/sevgi/geometry/elements/parallelogram.rb +59 -0
- data/lib/sevgi/geometry/elements/polygon.rb +7 -1
- data/lib/sevgi/geometry/elements/polyline.rb +7 -1
- data/lib/sevgi/geometry/elements/rect.rb +56 -8
- data/lib/sevgi/geometry/elements/triangle.rb +43 -0
- data/lib/sevgi/geometry/equation/linear.rb +147 -7
- data/lib/sevgi/geometry/equation/quadratic.rb +7 -1
- data/lib/sevgi/geometry/equation.rb +47 -9
- data/lib/sevgi/geometry/errors.rb +1 -0
- data/lib/sevgi/geometry/internal.rb +14 -0
- data/lib/sevgi/geometry/operation/align.rb +35 -9
- data/lib/sevgi/geometry/operation/sweep.rb +42 -5
- data/lib/sevgi/geometry/operation.rb +9 -0
- data/lib/sevgi/geometry/point.rb +103 -6
- data/lib/sevgi/geometry/segment.rb +87 -13
- data/lib/sevgi/geometry/version.rb +2 -1
- data/lib/sevgi/geometry.rb +9 -0
- metadata +5 -5
- data/lib/sevgi/geometry/elements/parm.rb +0 -37
- data/lib/sevgi/geometry/elements/tri.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 692e31b99ab3fb9e757badcf6b764bc5c788ed79a6d6955109e6edc44e9929b1
|
|
4
|
+
data.tar.gz: 92eb5d40058eeafafc625767e4abef2c201ee516fab38372bb1da440ec6c2c42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c741e60a6f2f37d0b6418cd2c574c9e6bae2bbc02ab0ee4835564bd49b9ddb68ceef420aacb62dfba25d405c0ad7c0db93efa1fa4c263839bb1dcb802010598
|
|
7
|
+
data.tar.gz: e596f27e84b36bcccabdb059c47641fdd468857595b60eda23592e4b69eccb59500159be5ece5b7f9a96e463bf4731a68ab13e7f1fedbe90094d71094fbe4b5a
|
data/README.md
CHANGED
|
@@ -2,47 +2,105 @@
|
|
|
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
|
|
|
10
22
|
# Core API
|
|
11
23
|
|
|
24
|
+
# Returns a copy moved to a point and optional offset.
|
|
25
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>, nil] target position, or nil to keep current position
|
|
26
|
+
# @param dx [Numeric] additional x offset
|
|
27
|
+
# @param dy [Numeric] additional y offset
|
|
28
|
+
# @return [Sevgi::Geometry::Element] translated element
|
|
29
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
12
30
|
def at(point = nil, dx: 0, dy: 0)
|
|
31
|
+
point = point ? Tuple[Point, point] : position
|
|
32
|
+
|
|
13
33
|
translate(
|
|
14
|
-
(
|
|
34
|
+
(point.x - position.x) + dx,
|
|
15
35
|
(point.y - position.y) + dy
|
|
16
36
|
)
|
|
17
37
|
end
|
|
18
38
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
# Returns the bounding rectangle.
|
|
40
|
+
# @abstract Subclasses implement element-specific bounds.
|
|
41
|
+
# @return [Sevgi::Geometry::Rect]
|
|
42
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement box
|
|
43
|
+
def box = PanicError.("#{self.class}#box must be implemented")
|
|
44
|
+
|
|
45
|
+
# Returns equations that define the element boundary.
|
|
46
|
+
# @abstract Subclasses implement element-specific equations.
|
|
47
|
+
# @return [Array<Sevgi::Geometry::Equation>]
|
|
48
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement equations
|
|
49
|
+
def equations = PanicError.("#{self.class}#equations must be implemented")
|
|
50
|
+
|
|
51
|
+
# Reports whether the element has zero bounding width and height.
|
|
52
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
53
|
+
# @return [Boolean]
|
|
23
54
|
def ignorable?(precision: nil) = F.zero?(box.width, precision:) && F.zero?(box.height, precision:)
|
|
24
55
|
|
|
25
|
-
|
|
56
|
+
# Returns the element position.
|
|
57
|
+
# @abstract Subclasses implement element-specific positioning.
|
|
58
|
+
# @return [Sevgi::Geometry::Point]
|
|
59
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement position
|
|
60
|
+
def position = PanicError.("#{self.class}#position must be implemented")
|
|
26
61
|
|
|
27
|
-
|
|
62
|
+
# Returns a translated copy.
|
|
63
|
+
# @abstract Subclasses implement element-specific translation.
|
|
64
|
+
# @param _x [Numeric] x offset
|
|
65
|
+
# @param _y [Numeric] y offset
|
|
66
|
+
# @return [Sevgi::Geometry::Element]
|
|
67
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement translate
|
|
68
|
+
def translate(_x, _y) = PanicError.("#{self.class}#translate must be implemented")
|
|
28
69
|
|
|
29
70
|
# rubocop:disable Metrics/ClassLength
|
|
71
|
+
# Element whose boundary is represented by straight segments.
|
|
30
72
|
class Lined < self
|
|
73
|
+
# Open lined element base class.
|
|
31
74
|
Open = Class.new(self) do
|
|
75
|
+
# Draws the element as an SVG polyline.
|
|
76
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
77
|
+
# @return [Object] graphics node command result
|
|
32
78
|
def draw!(node, **) = node.polyline(points: points.map { it.deconstruct.join(",") }, **)
|
|
33
79
|
end
|
|
34
80
|
|
|
81
|
+
# Closed lined element base class.
|
|
35
82
|
Close = Class.new(self) do
|
|
83
|
+
# Creates a closed element from points, appending the first point.
|
|
84
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
85
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
86
|
+
# @raise [Sevgi::Geometry::Error] when any point cannot be coerced
|
|
36
87
|
def self.new_by_points(*points) = super(*points, points.first)
|
|
37
88
|
|
|
89
|
+
# Draws the element as an SVG polygon.
|
|
90
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
91
|
+
# @return [Object] graphics node command result
|
|
38
92
|
def draw!(node, **) = node.polygon(points: points.map { it.deconstruct.join(",") }, **)
|
|
39
93
|
end
|
|
40
94
|
|
|
41
95
|
# Class methods
|
|
42
96
|
|
|
97
|
+
# Point shortcut names generated for fixed-size lined elements.
|
|
43
98
|
SHORTCUTS = ("A".."Z").to_a.freeze
|
|
44
99
|
|
|
45
|
-
#
|
|
100
|
+
# Builds a concrete lined element class.
|
|
101
|
+
# @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
|
|
102
|
+
# @param open [Boolean] true for an open path, false for a closed path
|
|
103
|
+
# @return [Class] lined element subclass
|
|
46
104
|
def self.build(size = Undefined, open: false)
|
|
47
105
|
Class.new(open ? Open : Close) do
|
|
48
106
|
define_singleton_method(:close?) { !open }
|
|
@@ -53,33 +111,64 @@ module Sevgi
|
|
|
53
111
|
|
|
54
112
|
define_singleton_method(:size) { size }
|
|
55
113
|
|
|
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
|
|
114
|
+
Lined.send(:define_shortcuts, self, size, open:) unless size == Undefined
|
|
67
115
|
end
|
|
68
|
-
# rubocop:enable Metrics/MethodLength
|
|
69
116
|
end
|
|
70
117
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
# @overload [](*segments, position: Origin)
|
|
119
|
+
# Builds an element from segments.
|
|
120
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
121
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
122
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
123
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
124
|
+
def self.[](...) = from_segments(...)
|
|
125
|
+
|
|
126
|
+
# @overload call(*points)
|
|
127
|
+
# Builds an element from points.
|
|
128
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
129
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
130
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
131
|
+
def self.call(...) = from_points(...)
|
|
132
|
+
|
|
133
|
+
# @overload from_points(*points)
|
|
134
|
+
# Builds an element from points.
|
|
135
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
136
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
137
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
138
|
+
def self.from_points(...) = new_by_points(...)
|
|
139
|
+
|
|
140
|
+
# @overload from_segments(*segments, position: Origin)
|
|
141
|
+
# Builds an element from segments.
|
|
142
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
143
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
144
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
145
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
146
|
+
def self.from_segments(...) = new_by_segments(...)
|
|
147
|
+
|
|
148
|
+
# @overload new_by_points(*points)
|
|
149
|
+
# Builds an element from points, applying closed-path behavior where appropriate.
|
|
150
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
151
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
152
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
75
153
|
def self.new_by_points(...) = new_by_points!(...)
|
|
76
154
|
|
|
155
|
+
# Builds an element from an exact point path.
|
|
156
|
+
#
|
|
157
|
+
# Closed classes require the closing point to be supplied by the caller.
|
|
158
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] exact boundary points
|
|
159
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
160
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced or do not satisfy the class path contract
|
|
77
161
|
def self.new_by_points!(*points)
|
|
78
162
|
new do
|
|
79
163
|
@points = Tuples[Point, *points]
|
|
80
164
|
end
|
|
81
165
|
end
|
|
82
166
|
|
|
167
|
+
# Builds an element from segments and a start position.
|
|
168
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
169
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
170
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
171
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
83
172
|
def self.new_by_segments(*segments, position: Origin)
|
|
84
173
|
new do
|
|
85
174
|
@position = Tuple[Point, position]
|
|
@@ -89,6 +178,29 @@ module Sevgi
|
|
|
89
178
|
|
|
90
179
|
private_class_method(:new)
|
|
91
180
|
|
|
181
|
+
def self.define_line_shortcuts(klass, point_names, open:)
|
|
182
|
+
line_names = point_names.each_cons(2).map(&:join)
|
|
183
|
+
line_names << "#{point_names.last}#{point_names.first}" if !open && point_names.any?
|
|
184
|
+
|
|
185
|
+
line_names.each_with_index do |name, i|
|
|
186
|
+
klass.define_method(name) { lines[i] or Error.("No such line: #{name}") }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.define_point_shortcuts(klass, point_names)
|
|
191
|
+
point_names.each_with_index do |name, i|
|
|
192
|
+
klass.define_method(name) { points[i] or Error.("No such point: #{name}") }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.define_shortcuts(klass, size, open:)
|
|
197
|
+
point_names = SHORTCUTS.first([open ? size + 1 : size, SHORTCUTS.size].min)
|
|
198
|
+
define_point_shortcuts(klass, point_names)
|
|
199
|
+
define_line_shortcuts(klass, point_names, open:)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method :define_line_shortcuts, :define_point_shortcuts, :define_shortcuts
|
|
203
|
+
|
|
92
204
|
def initialize(&block)
|
|
93
205
|
super()
|
|
94
206
|
|
|
@@ -104,6 +216,8 @@ module Sevgi
|
|
|
104
216
|
|
|
105
217
|
# Core methods
|
|
106
218
|
|
|
219
|
+
# Returns an element with approximate points and segments.
|
|
220
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
107
221
|
def approx
|
|
108
222
|
points, segments = points(true), segments(true)
|
|
109
223
|
self.class.send(:new) do
|
|
@@ -111,20 +225,33 @@ module Sevgi
|
|
|
111
225
|
end
|
|
112
226
|
end
|
|
113
227
|
|
|
228
|
+
# @overload draw(node, **attributes)
|
|
229
|
+
# Draws an approximate element into a graphics node.
|
|
230
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
231
|
+
# @param attributes [Hash] drawing attributes
|
|
232
|
+
# @return [Object] graphics node command result
|
|
114
233
|
def draw(...)
|
|
115
234
|
approx.draw!(...)
|
|
116
235
|
end
|
|
117
236
|
|
|
237
|
+
# Returns element points.
|
|
238
|
+
# @param approximate [Boolean] true to round points with the current function precision
|
|
239
|
+
# @return [Array<Sevgi::Geometry::Point>]
|
|
118
240
|
def points(approximate = false)
|
|
119
|
-
approximate ?
|
|
241
|
+
approximate ? @points.map(&:approx) : @points
|
|
120
242
|
end
|
|
121
243
|
|
|
244
|
+
# Returns the first point.
|
|
245
|
+
# @return [Sevgi::Geometry::Point]
|
|
122
246
|
def position
|
|
123
247
|
@position ||= points.first
|
|
124
248
|
end
|
|
125
249
|
|
|
250
|
+
# Returns element segments.
|
|
251
|
+
# @param approximate [Boolean] true to round segments with the current function precision
|
|
252
|
+
# @return [Array<Sevgi::Geometry::Segment>]
|
|
126
253
|
def segments(approximate = false)
|
|
127
|
-
approximate ?
|
|
254
|
+
approximate ? @segments.map(&:approx) : @segments
|
|
128
255
|
end
|
|
129
256
|
|
|
130
257
|
# Affinity methods
|
|
@@ -137,16 +264,29 @@ module Sevgi
|
|
|
137
264
|
|
|
138
265
|
# Equality methods
|
|
139
266
|
|
|
267
|
+
# Reports strict element equality by class and approximate points.
|
|
268
|
+
# @param other [Object] object to compare
|
|
269
|
+
# @return [Boolean]
|
|
140
270
|
def eql?(other) = self.class == other.class && points(true) == other.points(true)
|
|
141
271
|
|
|
272
|
+
# Returns a hash compatible with strict equality.
|
|
273
|
+
# @return [Integer]
|
|
142
274
|
def hash = [self.class, *points(true)].hash
|
|
143
275
|
|
|
144
276
|
alias == eql?
|
|
145
277
|
|
|
146
278
|
# Interaction methods
|
|
147
279
|
|
|
280
|
+
# Returns boundary equations for all lines.
|
|
281
|
+
# @return [Array<Sevgi::Geometry::Equation::Linear>]
|
|
148
282
|
def equations = @equations ||= lines.map(&:equation)
|
|
149
283
|
|
|
284
|
+
# Intersects the element boundary with an equation.
|
|
285
|
+
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
286
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
287
|
+
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
288
|
+
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
289
|
+
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
150
290
|
def intersection(equation, precision: nil)
|
|
151
291
|
equations
|
|
152
292
|
.map do |candidate|
|
|
@@ -158,30 +298,68 @@ module Sevgi
|
|
|
158
298
|
|
|
159
299
|
# Properties
|
|
160
300
|
|
|
301
|
+
# Returns a line by index.
|
|
302
|
+
# @param i [Integer] line index
|
|
303
|
+
# @return [Sevgi::Geometry::Line]
|
|
304
|
+
# @raise [Sevgi::Geometry::Error] when no line exists for index
|
|
161
305
|
def [](i) = lines[i].tap { |line| Error.("No line exist for index: #{i}") unless line }
|
|
162
306
|
|
|
163
|
-
|
|
307
|
+
# Returns the bounding rectangle.
|
|
308
|
+
# @return [Sevgi::Geometry::Rect]
|
|
309
|
+
def box = Rect.from_corners([(xs = points.map(&:x)).min, (ys = points.map(&:y)).min], [xs.max, ys.max])
|
|
164
310
|
|
|
311
|
+
# Returns a point by index.
|
|
312
|
+
# @param i [Integer] point index
|
|
313
|
+
# @return [Sevgi::Geometry::Point]
|
|
314
|
+
# @raise [Sevgi::Geometry::Error] when no point exists for index
|
|
165
315
|
def call(i) = points[i].tap { Error.("No point exist for index: #{i}") unless it }
|
|
166
316
|
|
|
317
|
+
# Returns the first segment.
|
|
318
|
+
# @return [Sevgi::Geometry::Segment]
|
|
167
319
|
def head = @head ||= segments.first
|
|
168
320
|
|
|
321
|
+
# Returns boundary lines derived from segments and points.
|
|
322
|
+
# @return [Array<Sevgi::Geometry::Line>]
|
|
169
323
|
def lines
|
|
170
324
|
@lines ||= segments.zip(points[...segments.size]).map { |segment, position|
|
|
171
325
|
segment.line(position)
|
|
172
326
|
}
|
|
173
327
|
end
|
|
174
328
|
|
|
329
|
+
# Returns the sum of segment lengths.
|
|
330
|
+
# @return [Float]
|
|
175
331
|
def perimeter = @perimeter ||= segments.sum(&:length)
|
|
176
332
|
|
|
333
|
+
# Returns the last segment.
|
|
334
|
+
# @return [Sevgi::Geometry::Segment]
|
|
177
335
|
def tail = @tail ||= segments.last
|
|
178
336
|
|
|
179
337
|
# Relation methods
|
|
180
338
|
|
|
181
|
-
|
|
339
|
+
# Reports whether a point is inside or on the boundary.
|
|
340
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
341
|
+
# @return [Boolean]
|
|
342
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
343
|
+
def inside?(point)
|
|
344
|
+
point = Tuple[Point, point]
|
|
345
|
+
|
|
346
|
+
on?(point) || pnpoly(points, point)
|
|
347
|
+
end
|
|
182
348
|
|
|
183
|
-
|
|
349
|
+
# Reports whether a point is on the boundary.
|
|
350
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
351
|
+
# @return [Boolean]
|
|
352
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
353
|
+
def on?(point)
|
|
354
|
+
point = Tuple[Point, point]
|
|
355
|
+
|
|
356
|
+
lines.any? { it.over?(point) }
|
|
357
|
+
end
|
|
184
358
|
|
|
359
|
+
# Reports whether a point is outside the element boundary.
|
|
360
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
361
|
+
# @return [Boolean]
|
|
362
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
185
363
|
def outside?(point) = !inside?(point)
|
|
186
364
|
|
|
187
365
|
private
|
|
@@ -236,17 +414,19 @@ module Sevgi
|
|
|
236
414
|
end
|
|
237
415
|
end
|
|
238
416
|
|
|
417
|
+
# Reserved base for future arced elements.
|
|
418
|
+
# @api private
|
|
239
419
|
class Arced < self
|
|
240
420
|
end
|
|
241
421
|
# rubocop:enable Metrics/ClassLength
|
|
242
422
|
end
|
|
243
423
|
|
|
244
424
|
require_relative "elements/line"
|
|
245
|
-
require_relative "elements/
|
|
425
|
+
require_relative "elements/parallelogram"
|
|
246
426
|
require_relative "elements/polygon"
|
|
247
427
|
require_relative "elements/polyline"
|
|
248
428
|
require_relative "elements/rect"
|
|
249
|
-
require_relative "elements/
|
|
429
|
+
require_relative "elements/triangle"
|
|
250
430
|
|
|
251
431
|
require_relative "elements/circle"
|
|
252
432
|
require_relative "elements/curve"
|
|
@@ -4,8 +4,37 @@ require "forwardable"
|
|
|
4
4
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Geometry
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# Generated superclass for Line.
|
|
8
|
+
# @api private
|
|
9
|
+
LineBase = Element.lined(1, open: true)
|
|
10
|
+
private_constant :LineBase
|
|
11
|
+
|
|
12
|
+
# Open lined element with one segment.
|
|
13
|
+
class Line < LineBase
|
|
14
|
+
# @overload [](length, angle, position: Origin)
|
|
15
|
+
# Builds a line from length and angle.
|
|
16
|
+
# @param length [Numeric] line length
|
|
17
|
+
# @param angle [Numeric] clockwise angle in degrees
|
|
18
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
19
|
+
# @return [Sevgi::Geometry::Line]
|
|
20
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
21
|
+
def self.[](...) = from_length_angle(...)
|
|
22
|
+
|
|
23
|
+
# Builds a line from length and angle.
|
|
24
|
+
# @param length [Numeric] line length
|
|
25
|
+
# @param angle [Numeric] clockwise angle in degrees
|
|
26
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
27
|
+
# @return [Sevgi::Geometry::Line]
|
|
28
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
29
|
+
def self.from_length_angle(length, angle, position: Origin) = new_by_segments(Segment[length, angle], position:)
|
|
30
|
+
|
|
31
|
+
# @overload from_points(starting, ending)
|
|
32
|
+
# Builds a line from two endpoints.
|
|
33
|
+
# @param starting [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
34
|
+
# @param ending [Sevgi::Geometry::Point, Array<Numeric>] ending point
|
|
35
|
+
# @return [Sevgi::Geometry::Line]
|
|
36
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
37
|
+
def self.from_points(...) = new_by_points(...)
|
|
9
38
|
|
|
10
39
|
extend Forwardable
|
|
11
40
|
|
|
@@ -16,15 +45,25 @@ module Sevgi
|
|
|
16
45
|
|
|
17
46
|
def_delegators :equation, :left?, :right?
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
# Draws the line into a graphics node.
|
|
49
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
50
|
+
# @return [Object] graphics node command result
|
|
51
|
+
def draw!(node, **) = node.LineTo(x1: position.x, y1: position.y, x2: ending.x, y2: ending.y, **)
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
# Reports whether a point lies on the finite line segment.
|
|
54
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
57
|
+
def over?(point)
|
|
58
|
+
point = Tuple[Point, point]
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
within_range?(point) && equation.on?(point)
|
|
61
|
+
end
|
|
26
62
|
|
|
27
|
-
|
|
63
|
+
# Returns a parallel line shifted by a signed perpendicular offset.
|
|
64
|
+
# @param distance [Numeric] signed perpendicular offset
|
|
65
|
+
# @return [Sevgi::Geometry::Line]
|
|
66
|
+
def shift(distance) = translate(distance * F.sin(angle), -distance * F.cos(angle))
|
|
28
67
|
|
|
29
68
|
private
|
|
30
69
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Geometry
|
|
5
|
+
# Generated superclass for Parallelogram.
|
|
6
|
+
# @api private
|
|
7
|
+
ParallelogramBase = Element.lined(4)
|
|
8
|
+
private_constant :ParallelogramBase
|
|
9
|
+
|
|
10
|
+
# Closed four-sided element built from horizontal and vertical segments.
|
|
11
|
+
class Parallelogram < ParallelogramBase
|
|
12
|
+
# Builds a parallelogram from adjacent horizontal and vertical segments.
|
|
13
|
+
# @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
|
|
14
|
+
# @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
|
|
15
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
16
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
17
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
18
|
+
def self.[](horizontal, vertical, position: Origin)
|
|
19
|
+
horizontal, vertical = Tuples[Segment, horizontal, vertical]
|
|
20
|
+
|
|
21
|
+
new_by_segments(horizontal, vertical.reverse, horizontal.reverse, vertical, position:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Builds a parallelogram from a horizontal segment and tallness constraint.
|
|
25
|
+
# @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
|
|
26
|
+
# @param tallness [Sevgi::Geometry::Polar, Array<Numeric>] target tallness as length and angle
|
|
27
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
28
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
29
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
|
|
30
|
+
def self.new_by_height(horizontal:, tallness:, position: Origin)
|
|
31
|
+
horizontal = Tuple[Segment, horizontal]
|
|
32
|
+
tallness = Tuple[Polar, tallness]
|
|
33
|
+
|
|
34
|
+
height = tallness.length - horizontal.y.abs
|
|
35
|
+
angle = tallness.angle
|
|
36
|
+
length = height / F.sin(angle)
|
|
37
|
+
|
|
38
|
+
self[horizontal, Segment[length, angle], position:]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Builds a parallelogram from a vertical segment and wideness constraint.
|
|
42
|
+
# @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
|
|
43
|
+
# @param wideness [Sevgi::Geometry::Polar, Array<Numeric>] target wideness as length and angle
|
|
44
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
45
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
46
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
|
|
47
|
+
def self.new_by_width(vertical:, wideness:, position: Origin)
|
|
48
|
+
vertical = Tuple[Segment, vertical]
|
|
49
|
+
wideness = Tuple[Polar, wideness]
|
|
50
|
+
|
|
51
|
+
width = wideness.length - vertical.x.abs
|
|
52
|
+
angle = wideness.angle
|
|
53
|
+
length = width / F.cos(angle)
|
|
54
|
+
|
|
55
|
+
self[Segment[length, angle], vertical, position:]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
5
|
+
# Generated superclass for Polygon.
|
|
6
|
+
# @api private
|
|
7
|
+
PolygonBase = Element.lined
|
|
8
|
+
private_constant :PolygonBase
|
|
9
|
+
|
|
10
|
+
# Variable-size closed lined element.
|
|
11
|
+
class Polygon < PolygonBase
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
5
|
+
# Generated superclass for Polyline.
|
|
6
|
+
# @api private
|
|
7
|
+
PolylineBase = Element.lined(open: true)
|
|
8
|
+
private_constant :PolylineBase
|
|
9
|
+
|
|
10
|
+
# Variable-size open lined element.
|
|
11
|
+
class Polyline < PolylineBase
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -2,8 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Generated superclass for Rect.
|
|
6
|
+
# @api private
|
|
7
|
+
RectBase = Element.lined(4)
|
|
8
|
+
private_constant :RectBase
|
|
9
|
+
|
|
10
|
+
# Closed four-sided rectangle aligned to the screen axes.
|
|
11
|
+
class Rect < RectBase
|
|
12
|
+
# @overload [](width, height, position: Origin)
|
|
13
|
+
# Builds a rectangle from size and top-left position.
|
|
14
|
+
# @param width [Numeric] rectangle width
|
|
15
|
+
# @param height [Numeric] rectangle height
|
|
16
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
17
|
+
# @return [Sevgi::Geometry::Rect]
|
|
18
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
19
|
+
def self.[](...) = from_size(...)
|
|
20
|
+
|
|
21
|
+
# @overload call(top_left, bottom_right)
|
|
22
|
+
# Builds a rectangle from two opposite corners.
|
|
23
|
+
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
24
|
+
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
25
|
+
# @return [Sevgi::Geometry::Rect]
|
|
26
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
27
|
+
def self.call(...) = from_corners(...)
|
|
28
|
+
|
|
29
|
+
# Builds a rectangle from two opposite corners.
|
|
30
|
+
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
31
|
+
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
32
|
+
# @return [Sevgi::Geometry::Rect]
|
|
33
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
34
|
+
def self.from_corners(top_left, bottom_right)
|
|
7
35
|
top_left, bottom_right = Tuples[Point, top_left, bottom_right]
|
|
8
36
|
width = (bottom_right.x - top_left.x).abs
|
|
9
37
|
|
|
@@ -15,20 +43,33 @@ module Sevgi
|
|
|
15
43
|
)
|
|
16
44
|
end
|
|
17
45
|
|
|
18
|
-
|
|
46
|
+
# Builds a rectangle from size and top-left position.
|
|
47
|
+
# @param width [Numeric] rectangle width
|
|
48
|
+
# @param height [Numeric] rectangle height
|
|
49
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
50
|
+
# @return [Sevgi::Geometry::Rect]
|
|
51
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
52
|
+
def self.from_size(width, height, position: Origin)
|
|
19
53
|
new_by_segments(
|
|
20
|
-
Segment.
|
|
21
|
-
Segment.
|
|
22
|
-
Segment.
|
|
23
|
-
Segment.
|
|
54
|
+
Segment.rightward(width),
|
|
55
|
+
Segment.downward(height),
|
|
56
|
+
Segment.leftward(width),
|
|
57
|
+
Segment.upward(height),
|
|
24
58
|
position:
|
|
25
59
|
)
|
|
26
60
|
end
|
|
27
61
|
|
|
62
|
+
# Draws the rectangle into a graphics node.
|
|
63
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
64
|
+
# @return [Object] graphics node command result
|
|
28
65
|
def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)
|
|
29
66
|
|
|
67
|
+
# Returns rectangle height.
|
|
68
|
+
# @return [Float]
|
|
30
69
|
def height = @height ||= segments[1].length
|
|
31
70
|
|
|
71
|
+
# Returns rectangle width.
|
|
72
|
+
# @return [Float]
|
|
32
73
|
def width = @width ||= segments[0].length
|
|
33
74
|
|
|
34
75
|
%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
|
|
@@ -40,10 +81,17 @@ module Sevgi
|
|
|
40
81
|
end
|
|
41
82
|
end
|
|
42
83
|
|
|
84
|
+
# Rectangle with equal width and height.
|
|
43
85
|
class Square < Rect
|
|
86
|
+
# @return [Float] side length
|
|
44
87
|
alias length width
|
|
45
88
|
|
|
46
|
-
|
|
89
|
+
# Builds a square from side length and top-left position.
|
|
90
|
+
# @param length [Numeric] side length
|
|
91
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
92
|
+
# @return [Sevgi::Geometry::Square]
|
|
93
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
94
|
+
def self.[](length, position: Origin) = from_size(length, length, position:)
|
|
47
95
|
end
|
|
48
96
|
end
|
|
49
97
|
end
|