sevgi-geometry 0.93.1 → 0.95.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.
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.0 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.
@@ -201,6 +203,11 @@ module Sevgi
201
203
 
202
204
  private_class_method :define_line_shortcuts, :define_point_shortcuts, :define_shortcuts
203
205
 
206
+ # Creates a lined element from a geometry-definition block.
207
+ # @yield evaluates point or segment definitions in the new element
208
+ # @yieldreturn [Object] ignored block result
209
+ # @return [void]
210
+ # @raise [Sevgi::Geometry::Error] when the block is absent or defines inconsistent geometry
204
211
  def initialize(&block)
205
212
  super()
206
213
 
@@ -210,6 +217,7 @@ module Sevgi
210
217
 
211
218
  @points ||= calculate_points_from_segments
212
219
  @segments ||= calculate_segments_from_points
220
+ freeze_geometry!
213
221
 
214
222
  sanitize
215
223
  end
@@ -234,11 +242,11 @@ module Sevgi
234
242
  approx.draw!(...)
235
243
  end
236
244
 
237
- # Returns element points.
245
+ # Returns immutable element points.
238
246
  # @param approximate [Boolean] true to round points with the current function precision
239
- # @return [Array<Sevgi::Geometry::Point>]
247
+ # @return [Array<Sevgi::Geometry::Point>] frozen point collection
240
248
  def points(approximate = false)
241
- approximate ? @points.map(&:approx) : @points
249
+ approximate ? @points.map(&:approx).freeze : @points
242
250
  end
243
251
 
244
252
  # Returns the first point.
@@ -247,15 +255,54 @@ module Sevgi
247
255
  @position ||= points.first
248
256
  end
249
257
 
250
- # Returns element segments.
258
+ # Returns immutable element segments.
251
259
  # @param approximate [Boolean] true to round segments with the current function precision
252
- # @return [Array<Sevgi::Geometry::Segment>]
260
+ # @return [Array<Sevgi::Geometry::Segment>] frozen segment collection
253
261
  def segments(approximate = false)
254
- approximate ? @segments.map(&:approx) : @segments
262
+ approximate ? @segments.map(&:approx).freeze : @segments
255
263
  end
256
264
 
257
265
  # Affinity methods
258
266
 
267
+ # @!parse
268
+ # # Returns an element reflected across the selected axes.
269
+ # # @param x [Boolean] reflect across the x-axis
270
+ # # @param y [Boolean] reflect across the y-axis
271
+ # # @return [Sevgi::Geometry::Element::Lined]
272
+ # def reflect(x: true, y: true); end
273
+ #
274
+ # # Returns an element rotated around the origin.
275
+ # # @param a [Numeric] clockwise angle in degrees
276
+ # # @return [Sevgi::Geometry::Element::Lined]
277
+ # def rotate(a); end
278
+ #
279
+ # # Returns an element scaled from the origin.
280
+ # # @param sx [Numeric] x scale factor
281
+ # # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
282
+ # # @return [Sevgi::Geometry::Element::Lined]
283
+ # def scale(sx, sy = Undefined); end
284
+ #
285
+ # # Returns an element skewed from the origin.
286
+ # # @param ax [Numeric] x-axis skew angle in degrees
287
+ # # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
288
+ # # @return [Sevgi::Geometry::Element::Lined]
289
+ # def skew(ax, ay = Undefined); end
290
+ #
291
+ # # Returns an element skewed along x.
292
+ # # @param a [Numeric] skew angle in degrees
293
+ # # @return [Sevgi::Geometry::Element::Lined]
294
+ # def skew_x(a); end
295
+ #
296
+ # # Returns an element skewed along y.
297
+ # # @param a [Numeric] skew angle in degrees
298
+ # # @return [Sevgi::Geometry::Element::Lined]
299
+ # def skew_y(a); end
300
+ #
301
+ # # Returns an element translated by offset.
302
+ # # @param dx [Numeric] x offset
303
+ # # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
304
+ # # @return [Sevgi::Geometry::Element::Lined]
305
+ # def translate(dx, dy = Undefined); end
259
306
  Geometry::Affinity.instance_methods.each do |transform|
260
307
  define_method(transform) do |*args, **kwargs, &block|
261
308
  self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
@@ -264,36 +311,50 @@ module Sevgi
264
311
 
265
312
  # Equality methods
266
313
 
267
- # Reports strict element equality by class and approximate points.
314
+ # Compares element points with optional numeric precision.
315
+ # @param other [Object] object to compare
316
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
317
+ # @return [Boolean]
318
+ def eq?(other, precision: nil)
319
+ other.instance_of?(self.class) &&
320
+ points.size == other.points.size &&
321
+ points.zip(other.points).all? { |left, right| left.eq?(right, precision:) }
322
+ end
323
+
324
+ # Reports strict element equality by class and exact points.
268
325
  # @param other [Object] object to compare
269
326
  # @return [Boolean]
270
- def eql?(other) = self.class == other.class && points(true) == other.points(true)
327
+ def eql?(other) = other.instance_of?(self.class) && points == other.points
271
328
 
272
329
  # Returns a hash compatible with strict equality.
273
330
  # @return [Integer]
274
- def hash = [self.class, *points(true)].hash
331
+ def hash = [self.class, *points].hash
275
332
 
276
333
  alias == eql?
277
334
 
278
335
  # Interaction methods
279
336
 
280
- # Returns boundary equations for all lines.
281
- # @return [Array<Sevgi::Geometry::Equation::Linear>]
282
- def equations = @equations ||= lines.map(&:equation)
337
+ # Returns immutable boundary equations for all lines.
338
+ # @return [Array<Sevgi::Geometry::Equation::Linear>] frozen equation collection
339
+ def equations = @equations ||= lines.map(&:equation).freeze
283
340
 
284
341
  # Intersects the element boundary with an equation.
342
+ #
343
+ # Boundary membership is tested on unrounded candidate points. `precision:`
344
+ # only rounds returned coordinates and controls duplicate collapse after
345
+ # membership has been accepted. When `precision` is nil, returned points use
346
+ # the current function precision.
285
347
  # @param equation [Sevgi::Geometry::Equation] equation to intersect with
286
- # @param precision [Integer, nil] decimal precision, or nil for the current function default
348
+ # @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
287
349
  # @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
288
350
  # @raise [Sevgi::Geometry::Error] when equation is not an equation
289
351
  # @raise [Sevgi::PanicError] when the equation combination is not implemented
290
352
  def intersection(equation, precision: nil)
291
- equations
292
- .map do |candidate|
293
- equation.intersect(candidate).map { |point| point.approx(precision) }.select { |point| on?(point) }
294
- end
295
- .flatten
296
- .uniq
353
+ points = equations.flat_map do |candidate|
354
+ equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
355
+ end
356
+
357
+ points.map { |point| point.approx(precision) }.uniq
297
358
  end
298
359
 
299
360
  # Properties
@@ -318,12 +379,15 @@ module Sevgi
318
379
  # @return [Sevgi::Geometry::Segment]
319
380
  def head = @head ||= segments.first
320
381
 
321
- # Returns boundary lines derived from segments and points.
322
- # @return [Array<Sevgi::Geometry::Line>]
382
+ # Returns immutable boundary lines derived from segments and points.
383
+ # @return [Array<Sevgi::Geometry::Line>] frozen line collection
323
384
  def lines
324
- @lines ||= segments.zip(points[...segments.size]).map { |segment, position|
325
- segment.line(position)
326
- }
385
+ @lines ||= segments
386
+ .zip(points[...segments.size])
387
+ .map { |segment, position|
388
+ segment.line(position)
389
+ }
390
+ .freeze
327
391
  end
328
392
 
329
393
  # Returns the sum of segment lengths.
@@ -337,12 +401,17 @@ module Sevgi
337
401
  # Relation methods
338
402
 
339
403
  # Reports whether a point is inside or on the boundary.
404
+ #
405
+ # Open paths have no filled interior; for them this predicate is true
406
+ # only for points on the actual path boundary.
340
407
  # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
341
408
  # @return [Boolean]
342
409
  # @raise [Sevgi::Geometry::Error] when point cannot be coerced
343
410
  def inside?(point)
344
411
  point = Tuple[Point, point]
345
412
 
413
+ return on?(point) if self.class.open?
414
+
346
415
  on?(point) || pnpoly(points, point)
347
416
  end
348
417
 
@@ -364,6 +433,12 @@ module Sevgi
364
433
 
365
434
  private
366
435
 
436
+ def boundary_point?(point, precision)
437
+ return on?(point) if precision.nil?
438
+
439
+ F.with_precision(precision) { on?(point) }
440
+ end
441
+
367
442
  def calculate_points_from_segments
368
443
  Error.("No segments found") unless segments
369
444
 
@@ -379,6 +454,11 @@ module Sevgi
379
454
  points.each_cons(2).map { Segment.(*it) }
380
455
  end
381
456
 
457
+ def freeze_geometry!
458
+ @points = @points.dup.freeze
459
+ @segments = @segments.dup.freeze
460
+ end
461
+
382
462
  # rubocop:disable Metrics/MethodLength
383
463
  # https://wrfranklin.org/Research/Short_Notes/pnpoly.html
384
464
  def pnpoly(vertices, test)
@@ -418,6 +498,8 @@ module Sevgi
418
498
  # @api private
419
499
  class Arced < self
420
500
  end
501
+
502
+ private_constant :Arced
421
503
  # rubocop:enable Metrics/ClassLength
422
504
  end
423
505
 
@@ -427,9 +509,5 @@ module Sevgi
427
509
  require_relative "elements/polyline"
428
510
  require_relative "elements/rect"
429
511
  require_relative "elements/triangle"
430
-
431
- require_relative "elements/circle"
432
- require_relative "elements/curve"
433
- require_relative "elements/ellipse"
434
512
  end
435
513
  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
44
+
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)
40
50
 
41
- def_delegators :head, :length, :angle
51
+ # Returns the line segment length.
52
+ # @return [Float]
53
+ def length = head.length
42
54
 
43
- def_delegator :points, :first, :starting
44
- def_delegator :points, :last, :ending
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
@@ -68,8 +85,6 @@ module Sevgi
68
85
  private
69
86
 
70
87
  def within_range?(point)
71
- return false if point.nan?
72
-
73
88
  point = point.approx
74
89
  points = [starting.approx, ending.approx]
75
90
 
@@ -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
@@ -7,21 +7,24 @@ module Sevgi
7
7
  class Linear < Equation
8
8
  # Non-axis-aligned linear equation in `y = slope * x + intercept` form.
9
9
  class Diagonal < Linear
10
- # @!attribute [r] slope
11
- # @return [Float] line slope
12
- # @!attribute [r] intercept
13
- # @return [Float] y-intercept
14
- attr_reader :slope, :intercept
10
+ # Returns the line slope.
11
+ # @return [Float]
12
+ attr_reader :slope
13
+
14
+ # Returns the y-intercept.
15
+ # @return [Float]
16
+ attr_reader :intercept
15
17
 
16
18
  # Creates a diagonal linear equation.
17
19
  # @param slope [Numeric] line slope
18
20
  # @param intercept [Numeric] y-intercept
19
21
  # @return [void]
22
+ # @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
20
23
  def initialize(slope:, intercept:)
21
24
  super()
22
25
 
23
- @slope = slope.to_f
24
- @intercept = intercept.to_f
26
+ @slope = Real[:slope, slope]
27
+ @intercept = Real[:intercept, intercept]
25
28
  end
26
29
 
27
30
  # Returns an equation rounded to precision.
@@ -116,6 +119,7 @@ module Sevgi
116
119
  # Creates a horizontal equation.
117
120
  # @param c [Numeric] y coordinate
118
121
  # @return [void]
122
+ # @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
119
123
  def initialize(c) = super(slope: 0.0, intercept: c)
120
124
 
121
125
  # Returns an equation rounded to precision.
@@ -146,10 +150,11 @@ module Sevgi
146
150
  # Creates a vertical equation.
147
151
  # @param c [Numeric] x coordinate
148
152
  # @return [void]
153
+ # @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
149
154
  def initialize(c)
150
155
  super()
151
156
 
152
- @x = c.to_f
157
+ @x = Real[:x, c]
153
158
  end
154
159
 
155
160
  # Returns an equation rounded to precision.
@@ -3,12 +3,16 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  class Equation
6
- # Base class for quadratic equations.
6
+ # Base class reserved for future quadratic equation support.
7
+ # @api private
7
8
  class Quadratic < Equation
8
9
  end
10
+
11
+ private_constant :Quadratic
9
12
  end
10
13
 
11
- # Circle equation support is not implemented yet.
14
+ # Reserved circle element support.
15
+ # @api private
12
16
  class Circle
13
17
  # Returns the circle equation.
14
18
  # @abstract Circle equation support is not implemented yet.
@@ -18,5 +22,7 @@ module Sevgi
18
22
  PanicError.("#{self.class}#equation must be implemented")
19
23
  end
20
24
  end
25
+
26
+ private_constant :Circle
21
27
  end
22
28
  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,39 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Validates numeric inputs that can be represented as finite Floats.
6
+ # @api private
7
+ module Real
8
+ # Coerces a public numeric value to a finite 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 Numeric, cannot be converted to Float, or is not finite
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
+ number = coerce(field, value)
19
+
20
+ unless number.is_a?(::Float) && number.finite?
21
+ Error.("Geometry #{field} must be finite: #{value.inspect}")
22
+ end
23
+
24
+ number
25
+ end
26
+
27
+ def self.coerce(field, value)
28
+ value.to_f
29
+ rescue ::StandardError => e
30
+ Error.("Geometry #{field} must be a finite Numeric: #{value.inspect} (#{e.message})")
31
+ end
32
+
33
+ private_class_method :coerce
34
+ end
35
+
36
+ private_constant :Real
37
+
5
38
  # Coerces array-like geometry inputs into typed tuple objects.
6
39
  # @api private
7
40
  module Tuple
@@ -13,8 +46,7 @@ module Sevgi
13
46
  def self.[](klass, arg)
14
47
  case arg
15
48
  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)
49
+ Error.("Array must have 2 elements: #{arg.inspect}") unless arg.size == 2
18
50
 
19
51
  klass.send(:new, *arg)
20
52
  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