sevgi-geometry 0.94.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 +275 -2
- data/LICENSE +672 -3
- data/README.md +7 -7
- data/lib/sevgi/geometry/element.rb +238 -121
- data/lib/sevgi/geometry/elements/line.rb +60 -17
- 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 +138 -74
- data/lib/sevgi/geometry/equation/quadratic.rb +4 -1
- data/lib/sevgi/geometry/equation.rb +36 -13
- data/lib/sevgi/geometry/internal.rb +19 -7
- data/lib/sevgi/geometry/operation/align.rb +1 -0
- data/lib/sevgi/geometry/operation/sweep.rb +24 -6
- data/lib/sevgi/geometry/operation.rb +28 -14
- data/lib/sevgi/geometry/point.rb +115 -21
- data/lib/sevgi/geometry/segment.rb +76 -21
- data/lib/sevgi/geometry/version.rb +1 -1
- data/lib/sevgi/geometry.rb +25 -2
- metadata +6 -6
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sevgi Geometry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sevgi Geometry provides the small set of geometric values and operations used by Sevgi drawings.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -23,15 +23,15 @@ rect.box.width
|
|
|
23
23
|
|
|
24
24
|
## Ruby compatibility
|
|
25
25
|
|
|
26
|
-
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the
|
|
26
|
+
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
27
27
|
|
|
28
28
|
## Native prerequisites
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
This gem needs only Ruby and its Ruby dependencies.
|
|
31
31
|
|
|
32
32
|
## Links
|
|
33
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
|
|
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,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,121 +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
|
|
|
274
|
+
# Creates a lined element from a geometry-definition block.
|
|
275
|
+
# @yield evaluates point or segment definitions in the new element
|
|
276
|
+
# @yieldreturn [Object] ignored block result
|
|
277
|
+
# @return [void]
|
|
278
|
+
# @raise [Sevgi::Geometry::Error] when the block is absent or defines inconsistent geometry
|
|
279
|
+
# @api private
|
|
206
280
|
def initialize(&block)
|
|
207
281
|
super()
|
|
208
282
|
|
|
@@ -215,18 +289,20 @@ module Sevgi
|
|
|
215
289
|
freeze_geometry!
|
|
216
290
|
|
|
217
291
|
sanitize
|
|
292
|
+
validate_geometry!
|
|
218
293
|
end
|
|
219
294
|
|
|
220
295
|
# Core methods
|
|
221
296
|
|
|
222
|
-
# 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
|
|
223
303
|
# @return [Sevgi::Geometry::Element::Lined]
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
self.class.send(:new) do
|
|
227
|
-
@points, @segments = points, segments
|
|
228
|
-
end
|
|
229
|
-
end
|
|
304
|
+
# @raise [Sevgi::ArgumentError] when precision is invalid
|
|
305
|
+
def approx(precision = nil) = self.class.send(:approximate, *rounded_points(precision))
|
|
230
306
|
|
|
231
307
|
# @overload draw(node, **attributes)
|
|
232
308
|
# Draws an approximate element into a graphics node.
|
|
@@ -234,14 +310,14 @@ module Sevgi
|
|
|
234
310
|
# @param attributes [Hash] drawing attributes
|
|
235
311
|
# @return [Object] graphics node command result
|
|
236
312
|
def draw(...)
|
|
237
|
-
approx.draw
|
|
313
|
+
approx.send(:draw!, ...)
|
|
238
314
|
end
|
|
239
315
|
|
|
240
316
|
# Returns immutable element points.
|
|
241
317
|
# @param approximate [Boolean] true to round points with the current function precision
|
|
242
318
|
# @return [Array<Sevgi::Geometry::Point>] frozen point collection
|
|
243
319
|
def points(approximate = false)
|
|
244
|
-
approximate ?
|
|
320
|
+
approximate ? rounded_points(nil) : @points
|
|
245
321
|
end
|
|
246
322
|
|
|
247
323
|
# Returns the first point.
|
|
@@ -254,53 +330,64 @@ module Sevgi
|
|
|
254
330
|
# @param approximate [Boolean] true to round segments with the current function precision
|
|
255
331
|
# @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
|
|
256
332
|
def segments(approximate = false)
|
|
257
|
-
approximate ?
|
|
333
|
+
approximate ? rounded_segments(nil) : @segments
|
|
258
334
|
end
|
|
259
335
|
|
|
260
336
|
# Affinity methods
|
|
261
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
|
+
|
|
262
341
|
# @!parse
|
|
263
342
|
# # Returns an element reflected across the selected axes.
|
|
264
343
|
# # @param x [Boolean] reflect across the x-axis
|
|
265
344
|
# # @param y [Boolean] reflect across the y-axis
|
|
266
345
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
346
|
+
# # @raise [Sevgi::Geometry::Error] when a flag is not Boolean
|
|
267
347
|
# def reflect(x: true, y: true); end
|
|
268
348
|
#
|
|
269
349
|
# # Returns an element rotated around the origin.
|
|
270
350
|
# # @param a [Numeric] clockwise angle in degrees
|
|
271
351
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
352
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
272
353
|
# def rotate(a); end
|
|
273
354
|
#
|
|
274
355
|
# # Returns an element scaled from the origin.
|
|
275
356
|
# # @param sx [Numeric] x scale factor
|
|
276
357
|
# # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
277
358
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
359
|
+
# # @raise [Sevgi::Geometry::Error] when a scale is not a finite real number
|
|
278
360
|
# def scale(sx, sy = Undefined); end
|
|
279
361
|
#
|
|
280
362
|
# # Returns an element skewed from the origin.
|
|
281
363
|
# # @param ax [Numeric] x-axis skew angle in degrees
|
|
282
364
|
# # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
283
365
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
366
|
+
# # @raise [Sevgi::Geometry::Error] when an angle is not a finite real number
|
|
284
367
|
# def skew(ax, ay = Undefined); end
|
|
285
368
|
#
|
|
286
369
|
# # Returns an element skewed along x.
|
|
287
370
|
# # @param a [Numeric] skew angle in degrees
|
|
288
371
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
372
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
289
373
|
# def skew_x(a); end
|
|
290
374
|
#
|
|
291
375
|
# # Returns an element skewed along y.
|
|
292
376
|
# # @param a [Numeric] skew angle in degrees
|
|
293
377
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
378
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
294
379
|
# def skew_y(a); end
|
|
295
380
|
#
|
|
296
381
|
# # Returns an element translated by offset.
|
|
297
382
|
# # @param dx [Numeric] x offset
|
|
298
383
|
# # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
299
384
|
# # @return [Sevgi::Geometry::Element::Lined]
|
|
385
|
+
# # @raise [Sevgi::Geometry::Error] when an offset is not a finite real number
|
|
300
386
|
# def translate(dx, dy = Undefined); end
|
|
301
|
-
|
|
387
|
+
Affinity.public_instance_methods(false).each do |transform|
|
|
302
388
|
define_method(transform) do |*args, **kwargs, &block|
|
|
303
|
-
|
|
389
|
+
transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
|
|
390
|
+
self.class.send(:affine, *transformed)
|
|
304
391
|
end
|
|
305
392
|
end
|
|
306
393
|
|
|
@@ -335,20 +422,25 @@ module Sevgi
|
|
|
335
422
|
|
|
336
423
|
# Intersects the element boundary with an equation.
|
|
337
424
|
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
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]]
|
|
342
431
|
# @param equation [Sevgi::Geometry::Equation] equation to intersect with
|
|
343
432
|
# @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
|
|
344
433
|
# @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
|
|
345
434
|
# @raise [Sevgi::Geometry::Error] when equation is not an equation
|
|
346
435
|
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
347
436
|
def intersection(equation, precision: nil)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.
|
|
437
|
+
Error.("Must be an equation: #{equation}") unless equation.is_a?(Equation)
|
|
438
|
+
|
|
439
|
+
points = equations.flat_map do |candidate|
|
|
440
|
+
equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
points.map { |point| point.approx(precision) }.uniq
|
|
352
444
|
end
|
|
353
445
|
|
|
354
446
|
# Properties
|
|
@@ -357,7 +449,7 @@ module Sevgi
|
|
|
357
449
|
# @param i [Integer] line index
|
|
358
450
|
# @return [Sevgi::Geometry::Line]
|
|
359
451
|
# @raise [Sevgi::Geometry::Error] when no line exists for index
|
|
360
|
-
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 }
|
|
361
453
|
|
|
362
454
|
# Returns the bounding rectangle.
|
|
363
455
|
# @return [Sevgi::Geometry::Rect]
|
|
@@ -367,7 +459,7 @@ module Sevgi
|
|
|
367
459
|
# @param i [Integer] point index
|
|
368
460
|
# @return [Sevgi::Geometry::Point]
|
|
369
461
|
# @raise [Sevgi::Geometry::Error] when no point exists for index
|
|
370
|
-
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 }
|
|
371
463
|
|
|
372
464
|
# Returns the first segment.
|
|
373
465
|
# @return [Sevgi::Geometry::Segment]
|
|
@@ -384,9 +476,9 @@ module Sevgi
|
|
|
384
476
|
.freeze
|
|
385
477
|
end
|
|
386
478
|
|
|
387
|
-
# Returns the
|
|
479
|
+
# Returns the total path length.
|
|
388
480
|
# @return [Float]
|
|
389
|
-
def
|
|
481
|
+
def length = @length ||= segments.sum(&:length)
|
|
390
482
|
|
|
391
483
|
# Returns the last segment.
|
|
392
484
|
# @return [Sevgi::Geometry::Segment]
|
|
@@ -398,13 +490,19 @@ module Sevgi
|
|
|
398
490
|
#
|
|
399
491
|
# Open paths have no filled interior; for them this predicate is true
|
|
400
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
|
|
401
499
|
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
402
500
|
# @return [Boolean]
|
|
403
501
|
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
404
502
|
def inside?(point)
|
|
405
503
|
point = Tuple[Point, point]
|
|
406
504
|
|
|
407
|
-
return on?(point)
|
|
505
|
+
return on?(point) unless self.class.send(:close?)
|
|
408
506
|
|
|
409
507
|
on?(point) || pnpoly(points, point)
|
|
410
508
|
end
|
|
@@ -427,11 +525,17 @@ module Sevgi
|
|
|
427
525
|
|
|
428
526
|
private
|
|
429
527
|
|
|
528
|
+
def boundary_point?(point, precision)
|
|
529
|
+
return on?(point) if precision.nil?
|
|
530
|
+
|
|
531
|
+
F.with_precision(precision) { on?(point) }
|
|
532
|
+
end
|
|
533
|
+
|
|
430
534
|
def calculate_points_from_segments
|
|
431
535
|
Error.("No segments found") unless segments
|
|
432
536
|
|
|
433
537
|
[point = position, *segments.map { point = it.ending(point) }].tap do |points|
|
|
434
|
-
#
|
|
538
|
+
# Share the first point object when the path closes within the current precision.
|
|
435
539
|
points[-1] = points.first if points.first.eq?(points.last)
|
|
436
540
|
end
|
|
437
541
|
end
|
|
@@ -472,14 +576,28 @@ module Sevgi
|
|
|
472
576
|
end
|
|
473
577
|
# rubocop:enable Metrics/MethodLength
|
|
474
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
|
+
|
|
475
589
|
def sanitize
|
|
476
|
-
np = self.class.poly? ? points.size : self.class.size + 1
|
|
590
|
+
np = self.class.send(:poly?) ? points.size : self.class.send(:size) + 1
|
|
477
591
|
ns = np - 1
|
|
478
592
|
|
|
479
593
|
Error.("Wrong number of points; expected #{np} where found #{points.size}") unless points.size == np
|
|
480
594
|
Error.("Wrong number of segments; expected #{ns} where found #{segments.size}") unless segments.size == ns
|
|
481
|
-
|
|
595
|
+
return unless self.class.send(:close?) && !points.first.eq?(points.last)
|
|
596
|
+
|
|
597
|
+
Error.("Element points must form a closed path")
|
|
482
598
|
end
|
|
599
|
+
|
|
600
|
+
def validate_geometry! = nil
|
|
483
601
|
end
|
|
484
602
|
|
|
485
603
|
# Reserved base for future arced elements.
|
|
@@ -488,7 +606,6 @@ module Sevgi
|
|
|
488
606
|
end
|
|
489
607
|
|
|
490
608
|
private_constant :Arced
|
|
491
|
-
# rubocop:enable Metrics/ClassLength
|
|
492
609
|
end
|
|
493
610
|
|
|
494
611
|
require_relative "elements/line"
|