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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9be3893bcfca34cd9d7a2014de7fbb21ba0720e34625c700dcd9dd20b16f1c52
4
- data.tar.gz: d9936cd27f1a5360163ef50ca926263b7d913bf3512b14006a504dd5c393d3aa
3
+ metadata.gz: 692e31b99ab3fb9e757badcf6b764bc5c788ed79a6d6955109e6edc44e9929b1
4
+ data.tar.gz: 92eb5d40058eeafafc625767e4abef2c201ee516fab38372bb1da440ec6c2c42
5
5
  SHA512:
6
- metadata.gz: 452bdaf29a65ce32032326de0968852cb1449e4faefb5fe3ee24fe6cb8e7ea335d5760264fa08505c6a83f6f763585d9fd4c859b278c6d008ba00be58c7e4b18
7
- data.tar.gz: 50f163f2a6d50e2bea7be781a45bba8a1dbdaaaf45a00004b73d8c58ac2306da54cf82a6418696fa11fa49ac47f562857f4b56a81007f0a73244cb9b67079713
6
+ metadata.gz: 6c741e60a6f2f37d0b6418cd2c574c9e6bae2bbc02ab0ee4835564bd49b9ddb68ceef420aacb62dfba25d405c0ad7c0db93efa1fa4c263839bb1dcb802010598
7
+ data.tar.gz: e596f27e84b36bcccabdb059c47641fdd468857595b60eda23592e4b69eccb59500159be5ece5b7f9a96e463bf4731a68ab13e7f1fedbe90094d71094fbe4b5a
data/README.md CHANGED
@@ -0,0 +1,5 @@
1
+ # Sevgi Geometry
2
+
3
+ Provides small geometry primitives and operations for Sevgi drawings.
4
+
5
+ See the root [README](../README.md) and the documentation site for usage.
@@ -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
- ((point ||= position).x - position.x) + dx,
34
+ (point.x - position.x) + dx,
15
35
  (point.y - position.y) + dy
16
36
  )
17
37
  end
18
38
 
19
- def box = raise NoMethodError, "#{self.class}#box must be implemented"
20
-
21
- def equations = raise NoMethodError, "#{self.class}#equations must be implemented"
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
- def position = raise NoMethodError, "#{self.class}#position must be implemented"
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
- def translate(_x, _y) = raise NoMethodError, "#{self.class}#translate must be implemented"
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
- # rubocop:disable Metrics/MethodLength
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
- def self.[](...) = new_by_segments(...)
72
-
73
- def self.call(...) = new_by_points(...)
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 ? (@points_approx ||= @points.map(&:approx)) : @points
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 ? (@segments_approx ||= @segments.map(&:approx)) : @segments
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
- def box = Rect.([(xs = points.map(&:x)).min, (ys = points.map(&:y)).min], [xs.max, ys.max])
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
- def inside?(point) = on?(point) || pnpoly(points, point)
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
- def on?(point) = lines.any? { it.over?(point) }
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/parm"
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/tri"
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
- class Line < Element.lined(1, open: true)
8
- def self.[](length, angle, position: Origin) = new_by_segments(Segment[length, angle], position:)
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
- alias a angle
20
- alias l length
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
- def draw!(node, **) = node.Cline(x1: position.x, y1: position.y, x2: ending.x, y2: ending.y, **)
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
- # TODO: xxx
25
- def over?(point) = within_range?(point) && equation.on?(point)
60
+ within_range?(point) && equation.on?(point)
61
+ end
26
62
 
27
- def shift(distance) = translate(distance * F.sin(angle), distance * F.cos(angle))
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
- class Polygon < Element.lined
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
- class Polyline < Element.lined(open: true)
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
- class Rect < Element.lined(4)
6
- def self.call(top_left, bottom_right)
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
- def self.[](width, height, position: Origin)
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.horizontal(width),
21
- Segment.vertical(height),
22
- Segment.horizontal!(width),
23
- Segment.vertical!(height),
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
- def self.[](length, position: Origin) = super(length, length, position:)
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