sevgi-geometry 0.95.0 → 0.98.2
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 +125 -0
- data/README.md +7 -7
- data/lib/sevgi/geometry/element.rb +222 -117
- data/lib/sevgi/geometry/elements/line.rb +60 -15
- data/lib/sevgi/geometry/elements/parallelogram.rb +110 -29
- data/lib/sevgi/geometry/elements/polygon.rb +41 -1
- data/lib/sevgi/geometry/elements/polyline.rb +36 -1
- data/lib/sevgi/geometry/elements/rect.rb +159 -29
- data/lib/sevgi/geometry/elements/triangle.rb +60 -11
- data/lib/sevgi/geometry/equation/linear.rb +131 -69
- data/lib/sevgi/geometry/equation.rb +36 -13
- data/lib/sevgi/geometry/internal.rb +3 -3
- data/lib/sevgi/geometry/operation/align.rb +1 -0
- data/lib/sevgi/geometry/operation/sweep.rb +20 -4
- data/lib/sevgi/geometry/operation.rb +28 -14
- data/lib/sevgi/geometry/point.rb +115 -13
- data/lib/sevgi/geometry/segment.rb +76 -13
- data/lib/sevgi/geometry/version.rb +1 -1
- data/lib/sevgi/geometry.rb +25 -2
- metadata +6 -6
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
#
|
|
5
|
+
# Abstract base class for positioned geometry values.
|
|
6
|
+
#
|
|
7
|
+
# Construct concrete shapes through their class factories. Element
|
|
8
|
+
# operations return new values, and {#box} supplies the axis-aligned bounds
|
|
9
|
+
# used by alignment and tiling helpers.
|
|
6
10
|
class Element
|
|
11
|
+
private_class_method :new
|
|
12
|
+
|
|
7
13
|
# @overload lined(size = Undefined, open: false)
|
|
8
14
|
# Builds a lined element subclass.
|
|
15
|
+
# Instances expose total path `length`; closed classes additionally expose `perimeter`.
|
|
9
16
|
# @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
|
|
10
17
|
# @param open [Boolean] true for an open path, false for a closed path
|
|
11
18
|
# @return [Class] subclass of {Sevgi::Geometry::Element::Lined}
|
|
12
|
-
|
|
19
|
+
# @raise [Sevgi::Geometry::Error] when size is not Undefined or a positive Integer, or open is not Boolean
|
|
20
|
+
# @example Define a custom two-segment open shape
|
|
21
|
+
# Path = Sevgi::Geometry::Element.lined(2, open: true)
|
|
22
|
+
# Path.([0, 0], [1, 0], [1, 1])
|
|
23
|
+
def self.lined(...) = Lined.send(:build, ...)
|
|
13
24
|
|
|
14
25
|
# @overload arced(*args)
|
|
15
26
|
# Builds an arced element subclass.
|
|
@@ -24,13 +35,20 @@ module Sevgi
|
|
|
24
35
|
# Core API
|
|
25
36
|
|
|
26
37
|
# Returns a copy moved to a point and optional offset.
|
|
38
|
+
# @example Position a copy without mutating the source
|
|
39
|
+
# source = Sevgi::Geometry::Rect[8, 4, position: [1, 2]]
|
|
40
|
+
# moved = source.at([10, 20], dx: 2)
|
|
41
|
+
# source.position.deconstruct # => [1.0, 2.0]
|
|
42
|
+
# moved.position.deconstruct # => [12.0, 20.0]
|
|
27
43
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>, nil] target position, or nil to keep current position
|
|
28
44
|
# @param dx [Numeric] additional x offset
|
|
29
45
|
# @param dy [Numeric] additional y offset
|
|
30
46
|
# @return [Sevgi::Geometry::Element] translated element
|
|
31
|
-
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
47
|
+
# @raise [Sevgi::Geometry::Error] when point or an offset cannot be coerced to finite geometry values
|
|
32
48
|
def at(point = nil, dx: 0, dy: 0)
|
|
33
49
|
point = point ? Tuple[Point, point] : position
|
|
50
|
+
dx = Real[:dx, dx]
|
|
51
|
+
dy = Real[:dy, dy]
|
|
34
52
|
|
|
35
53
|
translate(
|
|
36
54
|
(point.x - position.x) + dx,
|
|
@@ -69,18 +87,32 @@ module Sevgi
|
|
|
69
87
|
# @raise [Sevgi::PanicError] when a subclass does not implement translate
|
|
70
88
|
def translate(_x, _y) = PanicError.("#{self.class}#translate must be implemented")
|
|
71
89
|
|
|
72
|
-
# rubocop:disable Metrics/ClassLength
|
|
73
90
|
# Element whose boundary is represented by straight segments.
|
|
91
|
+
#
|
|
92
|
+
# The same path is available as immutable {#points}, {#segments}, and
|
|
93
|
+
# {#lines} collections. Closed shapes repeat their first point at the end;
|
|
94
|
+
# open paths do not. Only closed shapes have a filled interior, so
|
|
95
|
+
# `inside?` on an open path is equivalent to testing its boundary.
|
|
96
|
+
# @example Inspect the interchangeable point, segment, and line views
|
|
97
|
+
# rect = Sevgi::Geometry::Rect[8, 4]
|
|
98
|
+
# rect.points.size # => 5
|
|
99
|
+
# rect.segments.size # => 4
|
|
100
|
+
# rect.lines.size # => 4
|
|
101
|
+
# @see Sevgi::Geometry::Operation.sweep
|
|
74
102
|
class Lined < self
|
|
75
103
|
# Open lined element base class.
|
|
104
|
+
# @api private
|
|
76
105
|
Open = Class.new(self) do
|
|
77
106
|
# Draws the element as an SVG polyline.
|
|
78
107
|
# @param node [Object] graphics node receiving the drawing command
|
|
79
108
|
# @return [Object] graphics node command result
|
|
80
109
|
def draw!(node, **) = node.polyline(points: points.map { it.deconstruct.join(",") }, **)
|
|
110
|
+
|
|
111
|
+
private :draw!
|
|
81
112
|
end
|
|
82
113
|
|
|
83
114
|
# Closed lined element base class.
|
|
115
|
+
# @api private
|
|
84
116
|
Close = Class.new(self) do
|
|
85
117
|
# Creates a closed element from points, appending the first point.
|
|
86
118
|
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
@@ -88,126 +120,163 @@ module Sevgi
|
|
|
88
120
|
# @raise [Sevgi::Geometry::Error] when any point cannot be coerced
|
|
89
121
|
def self.new_by_points(*points) = super(*points, points.first)
|
|
90
122
|
|
|
123
|
+
# Returns the closed path perimeter.
|
|
124
|
+
# @return [Float]
|
|
125
|
+
def perimeter = length
|
|
126
|
+
|
|
91
127
|
# Draws the element as an SVG polygon.
|
|
92
128
|
# @param node [Object] graphics node receiving the drawing command
|
|
93
129
|
# @return [Object] graphics node command result
|
|
94
130
|
def draw!(node, **) = node.polygon(points: points.map { it.deconstruct.join(",") }, **)
|
|
131
|
+
|
|
132
|
+
private :draw!
|
|
133
|
+
|
|
134
|
+
private_class_method :new_by_points
|
|
95
135
|
end
|
|
96
136
|
|
|
97
137
|
# Class methods
|
|
98
138
|
|
|
99
139
|
# Point shortcut names generated for fixed-size lined elements.
|
|
140
|
+
# @api private
|
|
100
141
|
SHORTCUTS = ("A".."Z").to_a.freeze
|
|
142
|
+
private_constant :Close, :Open, :SHORTCUTS
|
|
143
|
+
|
|
144
|
+
class << self
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Builds a concrete lined element class.
|
|
148
|
+
# @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
|
|
149
|
+
# @param open [Boolean] true for an open path, false for a closed path
|
|
150
|
+
# @return [Class] lined element subclass
|
|
151
|
+
# @raise [Sevgi::Geometry::Error] when size is not Undefined or a positive Integer, or open is not Boolean
|
|
152
|
+
# @api private
|
|
153
|
+
def build(size = Undefined, open: false)
|
|
154
|
+
validate_factory(size, open)
|
|
155
|
+
|
|
156
|
+
klass = Class.new(open ? Open : Close)
|
|
157
|
+
klass.define_singleton_method(:close?) { !open }
|
|
158
|
+
klass.define_singleton_method(:poly?) { size.equal?(Undefined) }
|
|
159
|
+
klass.define_singleton_method(:size) { size }
|
|
160
|
+
define_shortcuts(klass, size, open:) unless size.equal?(Undefined)
|
|
161
|
+
klass.public_class_method(:[], :call, :from_points, :from_segments)
|
|
162
|
+
klass.private_class_method(:close?, :poly?, :size)
|
|
163
|
+
klass
|
|
164
|
+
end
|
|
101
165
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def self.build(size = Undefined, open: false)
|
|
107
|
-
Class.new(open ? Open : Close) do
|
|
108
|
-
define_singleton_method(:close?) { !open }
|
|
109
|
-
|
|
110
|
-
define_singleton_method(:open?) { open }
|
|
111
|
-
|
|
112
|
-
define_singleton_method(:poly?) { size == Undefined }
|
|
113
|
-
|
|
114
|
-
define_singleton_method(:size) { size }
|
|
166
|
+
def validate_factory(size, open)
|
|
167
|
+
unless size.equal?(Undefined) || (size.is_a?(::Integer) && size.positive?)
|
|
168
|
+
Error.("Lined segment count must be a positive Integer or Undefined")
|
|
169
|
+
end
|
|
115
170
|
|
|
116
|
-
|
|
171
|
+
Error.("Lined open flag must be Boolean") unless open.equal?(true) || open.equal?(false)
|
|
117
172
|
end
|
|
118
|
-
end
|
|
119
173
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
163
|
-
def self.new_by_points!(*points)
|
|
164
|
-
new do
|
|
165
|
-
@points = Tuples[Point, *points]
|
|
174
|
+
# @overload [](*segments, position: Origin)
|
|
175
|
+
# Builds an element from segments.
|
|
176
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
177
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
178
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
179
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
180
|
+
# @api private
|
|
181
|
+
def [](...) = new_by_segments(...)
|
|
182
|
+
|
|
183
|
+
# @overload call(*points)
|
|
184
|
+
# Builds an element from points.
|
|
185
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
186
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
187
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
188
|
+
# @api private
|
|
189
|
+
def call(...) = new_by_points(...)
|
|
190
|
+
|
|
191
|
+
# @overload from_points(*points)
|
|
192
|
+
# Builds an element from points.
|
|
193
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
194
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
195
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
196
|
+
# @api private
|
|
197
|
+
def from_points(...) = call(...)
|
|
198
|
+
|
|
199
|
+
# @overload from_segments(*segments, position: Origin)
|
|
200
|
+
# Builds an element from segments.
|
|
201
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
202
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
203
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
204
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
205
|
+
# @api private
|
|
206
|
+
def from_segments(*segments, position: Origin) = self[*segments, position:]
|
|
207
|
+
|
|
208
|
+
def affine(*points) = new_by_points!(*points)
|
|
209
|
+
|
|
210
|
+
def approximate(*points)
|
|
211
|
+
new_by_points!(*points)
|
|
212
|
+
rescue Error
|
|
213
|
+
(close? ? Polygon : Polyline).send(:new_by_points!, *points)
|
|
166
214
|
end
|
|
167
|
-
end
|
|
168
215
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
# @overload new_by_points(*points)
|
|
217
|
+
# Builds an element from points, applying closed-path behavior where appropriate.
|
|
218
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
|
|
219
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
220
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced
|
|
221
|
+
# @api private
|
|
222
|
+
def new_by_points(...) = new_by_points!(...)
|
|
223
|
+
|
|
224
|
+
# Builds an element from an exact point path.
|
|
225
|
+
#
|
|
226
|
+
# Closed classes require the closing point to be supplied by the caller.
|
|
227
|
+
# @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] exact boundary points
|
|
228
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
229
|
+
# @raise [Sevgi::Geometry::Error] when points cannot be coerced or do not satisfy the class path contract
|
|
230
|
+
# @api private
|
|
231
|
+
def new_by_points!(*points)
|
|
232
|
+
new do
|
|
233
|
+
@points = Tuples[Point, *points]
|
|
234
|
+
end
|
|
178
235
|
end
|
|
179
|
-
end
|
|
180
236
|
|
|
181
|
-
|
|
237
|
+
# Builds an element from segments and a start position.
|
|
238
|
+
# @param segments [Array<Sevgi::Geometry::Segment, Array<Numeric>>] boundary segments
|
|
239
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
240
|
+
# @return [Sevgi::Geometry::Element::Lined]
|
|
241
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
242
|
+
# @api private
|
|
243
|
+
def new_by_segments(*segments, position: Origin)
|
|
244
|
+
new do
|
|
245
|
+
@position = Tuple[Point, position]
|
|
246
|
+
@segments = Tuples[Segment, *segments]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
182
249
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
250
|
+
def define_line_shortcuts(klass, point_names, closing:)
|
|
251
|
+
line_names = point_names.each_cons(2).map(&:join)
|
|
252
|
+
line_names << "#{point_names.last}#{point_names.first}" if closing
|
|
186
253
|
|
|
187
|
-
|
|
188
|
-
|
|
254
|
+
line_names.each_with_index do |name, i|
|
|
255
|
+
klass.define_method(name) { lines[i] or Error.("No such line: #{name}") }
|
|
256
|
+
end
|
|
189
257
|
end
|
|
190
|
-
end
|
|
191
258
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
259
|
+
def define_point_shortcuts(klass, point_names)
|
|
260
|
+
point_names.each_with_index do |name, i|
|
|
261
|
+
klass.define_method(name) { points[i] or Error.("No such point: #{name}") }
|
|
262
|
+
end
|
|
195
263
|
end
|
|
196
|
-
end
|
|
197
264
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
265
|
+
def define_shortcuts(klass, size, open:)
|
|
266
|
+
point_names = SHORTCUTS.first([open ? size + 1 : size, SHORTCUTS.size].min)
|
|
267
|
+
define_point_shortcuts(klass, point_names)
|
|
268
|
+
define_line_shortcuts(klass, point_names, closing: !open && point_names.size == size)
|
|
269
|
+
end
|
|
202
270
|
end
|
|
203
271
|
|
|
204
|
-
private_class_method :
|
|
272
|
+
private_class_method :new
|
|
205
273
|
|
|
206
274
|
# Creates a lined element from a geometry-definition block.
|
|
207
275
|
# @yield evaluates point or segment definitions in the new element
|
|
208
276
|
# @yieldreturn [Object] ignored block result
|
|
209
277
|
# @return [void]
|
|
210
278
|
# @raise [Sevgi::Geometry::Error] when the block is absent or defines inconsistent geometry
|
|
279
|
+
# @api private
|
|
211
280
|
def initialize(&block)
|
|
212
281
|
super()
|
|
213
282
|
|
|
@@ -220,18 +289,20 @@ module Sevgi
|
|
|
220
289
|
freeze_geometry!
|
|
221
290
|
|
|
222
291
|
sanitize
|
|
292
|
+
validate_geometry!
|
|
223
293
|
end
|
|
224
294
|
|
|
225
295
|
# Core methods
|
|
226
296
|
|
|
227
|
-
# Returns an element
|
|
297
|
+
# Returns an element rebuilt from rounded boundary points.
|
|
298
|
+
#
|
|
299
|
+
# Segments are derived from the rounded points so both representations describe the same path. When rounding
|
|
300
|
+
# breaks a concrete shape invariant, the result widens to a less specific Rect, Polygon, or Polyline rather than
|
|
301
|
+
# retaining a misleading concrete class.
|
|
302
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
228
303
|
# @return [Sevgi::Geometry::Element::Lined]
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
self.class.send(:new) do
|
|
232
|
-
@points, @segments = points, segments
|
|
233
|
-
end
|
|
234
|
-
end
|
|
304
|
+
# @raise [Sevgi::ArgumentError] when precision is invalid
|
|
305
|
+
def approx(precision = nil) = self.class.send(:approximate, *rounded_points(precision))
|
|
235
306
|
|
|
236
307
|
# @overload draw(node, **attributes)
|
|
237
308
|
# Draws an approximate element into a graphics node.
|
|
@@ -239,14 +310,14 @@ module Sevgi
|
|
|
239
310
|
# @param attributes [Hash] drawing attributes
|
|
240
311
|
# @return [Object] graphics node command result
|
|
241
312
|
def draw(...)
|
|
242
|
-
approx.draw
|
|
313
|
+
approx.send(:draw!, ...)
|
|
243
314
|
end
|
|
244
315
|
|
|
245
316
|
# Returns immutable element points.
|
|
246
317
|
# @param approximate [Boolean] true to round points with the current function precision
|
|
247
318
|
# @return [Array<Sevgi::Geometry::Point>] frozen point collection
|
|
248
319
|
def points(approximate = false)
|
|
249
|
-
approximate ?
|
|
320
|
+
approximate ? rounded_points(nil) : @points
|
|
250
321
|
end
|
|
251
322
|
|
|
252
323
|
# Returns the first point.
|
|
@@ -259,53 +330,64 @@ module Sevgi
|
|
|
259
330
|
# @param approximate [Boolean] true to round segments with the current function precision
|
|
260
331
|
# @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
|
|
261
332
|
def segments(approximate = false)
|
|
262
|
-
approximate ?
|
|
333
|
+
approximate ? rounded_segments(nil) : @segments
|
|
263
334
|
end
|
|
264
335
|
|
|
265
336
|
# Affinity methods
|
|
266
337
|
|
|
338
|
+
# Affine operations preserve a shape class while its semantic invariant still holds. Axis-aligned Rect and
|
|
339
|
+
# Square instances widen to Rect or Parallelogram when rotation, skew, or unequal scaling changes that category.
|
|
340
|
+
|
|
267
341
|
# @!parse
|
|
268
342
|
# # Returns an element reflected across the selected axes.
|
|
269
343
|
# # @param x [Boolean] reflect across the x-axis
|
|
270
344
|
# # @param y [Boolean] reflect across the y-axis
|
|
271
345
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
346
|
+
# # @raise [Sevgi::Geometry::Error] when a flag is not Boolean
|
|
272
347
|
# def reflect(x: true, y: true); end
|
|
273
348
|
#
|
|
274
349
|
# # Returns an element rotated around the origin.
|
|
275
350
|
# # @param a [Numeric] clockwise angle in degrees
|
|
276
351
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
352
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
277
353
|
# def rotate(a); end
|
|
278
354
|
#
|
|
279
355
|
# # Returns an element scaled from the origin.
|
|
280
356
|
# # @param sx [Numeric] x scale factor
|
|
281
357
|
# # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
282
358
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
359
|
+
# # @raise [Sevgi::Geometry::Error] when a scale is not a finite real number
|
|
283
360
|
# def scale(sx, sy = Undefined); end
|
|
284
361
|
#
|
|
285
362
|
# # Returns an element skewed from the origin.
|
|
286
363
|
# # @param ax [Numeric] x-axis skew angle in degrees
|
|
287
364
|
# # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
288
365
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
366
|
+
# # @raise [Sevgi::Geometry::Error] when an angle is not a finite real number
|
|
289
367
|
# def skew(ax, ay = Undefined); end
|
|
290
368
|
#
|
|
291
369
|
# # Returns an element skewed along x.
|
|
292
370
|
# # @param a [Numeric] skew angle in degrees
|
|
293
371
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
372
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
294
373
|
# def skew_x(a); end
|
|
295
374
|
#
|
|
296
375
|
# # Returns an element skewed along y.
|
|
297
376
|
# # @param a [Numeric] skew angle in degrees
|
|
298
377
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
378
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
299
379
|
# def skew_y(a); end
|
|
300
380
|
#
|
|
301
381
|
# # Returns an element translated by offset.
|
|
302
382
|
# # @param dx [Numeric] x offset
|
|
303
383
|
# # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
304
384
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
385
|
+
# # @raise [Sevgi::Geometry::Error] when an offset is not a finite real number
|
|
305
386
|
# def translate(dx, dy = Undefined); end
|
|
306
|
-
|
|
387
|
+
Affinity.public_instance_methods(false).each do |transform|
|
|
307
388
|
define_method(transform) do |*args, **kwargs, &block|
|
|
308
|
-
|
|
389
|
+
transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
|
|
390
|
+
self.class.send(:affine, *transformed)
|
|
309
391
|
end
|
|
310
392
|
end
|
|
311
393
|
|
|
@@ -340,16 +422,20 @@ module Sevgi
|
|
|
340
422
|
|
|
341
423
|
# Intersects the element boundary with an equation.
|
|
342
424
|
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
425
|
+
# Precision is applied consistently to boundary-membership tolerance, returned-coordinate rounding, and
|
|
426
|
+
# duplicate collapse. A nil precision uses the current thread's function precision for all three stages.
|
|
427
|
+
# @example Intersect a rectangle with a vertical line
|
|
428
|
+
# rect = Sevgi::Geometry::Rect[8, 4]
|
|
429
|
+
# axis = Sevgi::Geometry::Equation.vertical(3)
|
|
430
|
+
# rect.intersection(axis).map(&:deconstruct) # => [[3.0, 0.0], [3.0, 4.0]]
|
|
347
431
|
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
348
432
|
# @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
|
|
349
433
|
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
350
434
|
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
351
435
|
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
352
436
|
def intersection(equation, precision: nil)
|
|
437
|
+
Error.("Must be an equation: #{equation}") unless equation.is_a?(Equation)
|
|
438
|
+
|
|
353
439
|
points = equations.flat_map do |candidate|
|
|
354
440
|
equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
|
|
355
441
|
end
|
|
@@ -363,7 +449,7 @@ module Sevgi
|
|
|
363
449
|
# @param i [Integer] line index
|
|
364
450
|
# @return [Sevgi::Geometry::Line]
|
|
365
451
|
# @raise [Sevgi::Geometry::Error] when no line exists for index
|
|
366
|
-
def [](i) = lines[i].tap { |line| Error.("No line
|
|
452
|
+
def [](i) = lines[i].tap { |line| Error.("No line exists for index: #{i}") unless line }
|
|
367
453
|
|
|
368
454
|
# Returns the bounding rectangle.
|
|
369
455
|
# @return [Sevgi::Geometry::Rect]
|
|
@@ -373,7 +459,7 @@ module Sevgi
|
|
|
373
459
|
# @param i [Integer] point index
|
|
374
460
|
# @return [Sevgi::Geometry::Point]
|
|
375
461
|
# @raise [Sevgi::Geometry::Error] when no point exists for index
|
|
376
|
-
def call(i) = points[i].tap { Error.("No point
|
|
462
|
+
def call(i) = points[i].tap { Error.("No point exists for index: #{i}") unless it }
|
|
377
463
|
|
|
378
464
|
# Returns the first segment.
|
|
379
465
|
# @return [Sevgi::Geometry::Segment]
|
|
@@ -390,9 +476,9 @@ module Sevgi
|
|
|
390
476
|
.freeze
|
|
391
477
|
end
|
|
392
478
|
|
|
393
|
-
# Returns the
|
|
479
|
+
# Returns the total path length.
|
|
394
480
|
# @return [Float]
|
|
395
|
-
def
|
|
481
|
+
def length = @length ||= segments.sum(&:length)
|
|
396
482
|
|
|
397
483
|
# Returns the last segment.
|
|
398
484
|
# @return [Sevgi::Geometry::Segment]
|
|
@@ -404,13 +490,19 @@ module Sevgi
|
|
|
404
490
|
#
|
|
405
491
|
# Open paths have no filled interior; for them this predicate is true
|
|
406
492
|
# only for points on the actual path boundary.
|
|
493
|
+
# @example Compare closed and open path containment
|
|
494
|
+
# rect = Sevgi::Geometry::Rect[8, 4]
|
|
495
|
+
# line = Sevgi::Geometry::Line.([0, 0], [8, 0])
|
|
496
|
+
# rect.inside?([4, 2]) # => true
|
|
497
|
+
# line.inside?([4, 2]) # => false
|
|
498
|
+
# line.inside?([4, 0]) # => true
|
|
407
499
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
408
500
|
# @return [Boolean]
|
|
409
501
|
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
410
502
|
def inside?(point)
|
|
411
503
|
point = Tuple[Point, point]
|
|
412
504
|
|
|
413
|
-
return on?(point)
|
|
505
|
+
return on?(point) unless self.class.send(:close?)
|
|
414
506
|
|
|
415
507
|
on?(point) || pnpoly(points, point)
|
|
416
508
|
end
|
|
@@ -443,7 +535,7 @@ module Sevgi
|
|
|
443
535
|
Error.("No segments found") unless segments
|
|
444
536
|
|
|
445
537
|
[point = position, *segments.map { point = it.ending(point) }].tap do |points|
|
|
446
|
-
#
|
|
538
|
+
# Share the first point object when the path closes within the current precision.
|
|
447
539
|
points[-1] = points.first if points.first.eq?(points.last)
|
|
448
540
|
end
|
|
449
541
|
end
|
|
@@ -484,14 +576,28 @@ module Sevgi
|
|
|
484
576
|
end
|
|
485
577
|
# rubocop:enable Metrics/MethodLength
|
|
486
578
|
|
|
579
|
+
def rounded_points(precision)
|
|
580
|
+
rounded = @points.map { it.approx(precision) }
|
|
581
|
+
rounded[-1] = rounded.first if self.class.send(:close?)
|
|
582
|
+
rounded.freeze
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def rounded_segments(precision)
|
|
586
|
+
rounded_points(precision).each_cons(2).map { Segment.(*it) }.freeze
|
|
587
|
+
end
|
|
588
|
+
|
|
487
589
|
def sanitize
|
|
488
|
-
np = self.class.poly? ? points.size : self.class.size + 1
|
|
590
|
+
np = self.class.send(:poly?) ? points.size : self.class.send(:size) + 1
|
|
489
591
|
ns = np - 1
|
|
490
592
|
|
|
491
593
|
Error.("Wrong number of points; expected #{np} where found #{points.size}") unless points.size == np
|
|
492
594
|
Error.("Wrong number of segments; expected #{ns} where found #{segments.size}") unless segments.size == ns
|
|
493
|
-
|
|
595
|
+
return unless self.class.send(:close?) && !points.first.eq?(points.last)
|
|
596
|
+
|
|
597
|
+
Error.("Element points must form a closed path")
|
|
494
598
|
end
|
|
599
|
+
|
|
600
|
+
def validate_geometry! = nil
|
|
495
601
|
end
|
|
496
602
|
|
|
497
603
|
# Reserved base for future arced elements.
|
|
@@ -500,7 +606,6 @@ module Sevgi
|
|
|
500
606
|
end
|
|
501
607
|
|
|
502
608
|
private_constant :Arced
|
|
503
|
-
# rubocop:enable Metrics/ClassLength
|
|
504
609
|
end
|
|
505
610
|
|
|
506
611
|
require_relative "elements/line"
|