sevgi-geometry 0.93.1 → 0.94.0

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