sevgi-geometry 0.98.2 → 1.0.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: 7928a9adb69da869093c319a3a755110f5e12e5b6f336c949842194c58ffabca
4
- data.tar.gz: 35127c296da46e52a74b37b9535a49cf2717b51bad2fa83178734914160ecbec
3
+ metadata.gz: 5541e9628216ffd94f9d305d00419f6d7d306c6c0250cdece01c04907cf2e504
4
+ data.tar.gz: 0d1f0f9ea6bb347e8b98e9472c66ac87ddc9a2e512160c73848788ec5b36dc7b
5
5
  SHA512:
6
- metadata.gz: 4c67d947c34fedd4ac99723bb070413d33300030a50a450cabea9f9bea85aed9837d54d792797b989cd4310bdcfca2988be46bac7087edbf96fa378152982145
7
- data.tar.gz: d4f55fbf8282515f524ff9056a02c4e629aa014dcce4435066996bdd2cbd8c9edcb72979ed98d6c6430f8f636722a98686f5e8710e0983461fc32d80ec38c56d
6
+ metadata.gz: e5c264c3685c2b0034ab38c0ff64d36b53de4adec41df40d98609ce18841cca60922c24d1e0b331b931456a40e9ed519ec806ad566a35f47c68ffd6b4c7ead74
7
+ data.tar.gz: f053552d2951ab8147cdfb5cbb974b080fd0780885a316f148386fce0d6d2680da3021853772e41b12e84832c742c3536e9df3f9b710c25b838646457846ee03
data/CHANGELOG.md CHANGED
@@ -3,7 +3,101 @@
3
3
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
- ## Unreleased
6
+ ## 1.0.0 - 2026-09-16
7
+
8
+ ### Added
9
+
10
+ - Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
11
+
12
+ ### Changed
13
+
14
+ - Matched documentation page widths to the homepage Showcase.
15
+ - Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
16
+ - Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
17
+ - Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
18
+ - Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
19
+ - Made Segment equality compare length and direction; ordering still compares length only.
20
+ - Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
21
+ - Simplified the first-run guide and aligned library constructor examples.
22
+ - Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
23
+ - Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
24
+ - Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
25
+ - Documented non-rendering element data and clarified the related DSL catalog examples.
26
+ - Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
27
+ - Removed an obsolete ruler example and unused site code.
28
+ - Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
29
+ - Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
30
+
31
+ ### Fixed
32
+
33
+ - Kept horizontal scrollbars visible in Showcase example code panels.
34
+ - Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
35
+ - Preserved open-path endpoints and derived edges independently of display precision.
36
+ - Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
37
+ - Corrected the Showcase social image dimensions.
38
+ - Added SVG namespaces to standalone drawing examples and regenerated their outputs.
39
+ - Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
40
+ - Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
41
+ - Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
42
+ - Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
43
+ - Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
44
+ - Prevented avoidable overflow and underflow in point distances.
45
+ - Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
46
+ - Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
47
+ - Restored Showcase tab icons with embedded Font Awesome SVGs.
48
+
49
+ ## 0.100.0 - 2026-09-09
50
+
51
+ ### Added
52
+
53
+ - Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
54
+ - Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
55
+ linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
56
+ - Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
57
+ - Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
58
+ polygon `simple?`, `convex?`, and `concave?` predicates.
59
+ - Added Protractor and Arc examples and documented the drawing and calculation APIs.
60
+
61
+ ### Changed
62
+
63
+ - Removed redundant parent scans while duplicating element subtrees.
64
+ - Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
65
+
66
+ ### Fixed
67
+
68
+ - Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
69
+ - Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
70
+ Derender output and evaluation.
71
+ - Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
72
+ - Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
73
+ - Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
74
+ - Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
75
+ - Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
76
+ - Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
77
+ - Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
78
+ - Rejected nil and false aggregate-box inputs with the documented geometry error.
79
+ - Restored precision-aware rejection of zero-length Triangle sides.
80
+ - Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
81
+ - Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
82
+
83
+ ## 0.99.0 - 2026-08-09
84
+
85
+ ### Added
86
+
87
+ - Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
88
+ - Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
89
+
90
+ ### Changed
91
+
92
+ - Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
93
+ - Documented how the `igves` and `igsev` command names reflect their conversion directions.
94
+ - Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
95
+
96
+ ### Fixed
97
+
98
+ - Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
99
+ - Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
100
+ - Removed duplicate provider and context labels from DSL catalog entries.
7
101
 
8
102
  ## 0.98.2 - 2026-07-25
9
103
 
@@ -212,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
212
306
 
213
307
  ### Changed
214
308
 
215
- - Added the checker board example to the documentation showcase flow.
309
+ - Added the checkerboard example to the documentation showcase flow.
216
310
  - Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
217
311
 
218
312
  ### Fixed
data/README.md CHANGED
@@ -18,9 +18,34 @@ require "sevgi/geometry"
18
18
 
19
19
  ```ruby
20
20
  rect = Sevgi::Geometry::Rect[3, 5]
21
- rect.box.width
21
+ rect.center # => Point[1.5, 2.5]
22
+ rect.closed? # => true
23
+ rect.vertices.size # => 4
24
+ rect.points.size # => 5
25
+ rect.translate(2, 1).position # => Point[2.0, 1.0]
26
+
27
+ Sevgi::Geometry::Point.midpoint([0, 0], [3, 5]) # => Point[1.5, 2.5]
28
+ ```
29
+
30
+ Geometry uses SVG screen coordinates. Positive x goes right, positive y goes down, and positive angles turn clockwise.
31
+ Operations return new immutable values.
32
+
33
+ Open lined paths expose `starting`, `ending`, and `reverse`. Use `closed?` to distinguish open and closed boundaries.
34
+ Closed lined paths repeat their first vertex at the end of `points`. `vertices` omits that path-closing repetition.
35
+
36
+ `Ellipse` and `Circle` describe closed boundaries. `Arc` selects a finite, directed part of an ellipse:
37
+
38
+ ```ruby
39
+ ellipse = Sevgi::Geometry::Ellipse[40, 20, position: [50, 30]]
40
+ arc = ellipse.arc starting_angle: 180, extent: 120
41
+ arc.box
42
+ arc.length
43
+ arc.intersection Sevgi::Geometry::Equation.vertical(50)
22
44
  ```
23
45
 
46
+ Positive extent turns clockwise. Its absolute value must be less than 360 degrees.
47
+ Bounds and length do not depend on display precision. Closed ellipses and circles also support line sweeps.
48
+
24
49
  ## Ruby compatibility
25
50
 
26
51
  Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
@@ -12,7 +12,7 @@ module Sevgi
12
12
 
13
13
  # @overload lined(size = Undefined, open: false)
14
14
  # Builds a lined element subclass.
15
- # Instances expose total path `length`; closed classes additionally expose `perimeter`.
15
+ # Instances expose total path `length`. Closed classes also expose `perimeter`.
16
16
  # @param size [Integer, Sevgi::Undefined] segment count for fixed-size elements, or Undefined for variable size
17
17
  # @param open [Boolean] true for an open path, false for a closed path
18
18
  # @return [Class] subclass of {Sevgi::Geometry::Element::Lined}
@@ -22,16 +22,6 @@ module Sevgi
22
22
  # Path.([0, 0], [1, 0], [1, 1])
23
23
  def self.lined(...) = Lined.send(:build, ...)
24
24
 
25
- # @overload arced(*args)
26
- # Builds an arced element subclass.
27
- # @api private
28
- # @param args [Array<Object>] arced factory arguments
29
- # @return [Class]
30
- # @raise [NoMethodError] until arced elements are implemented
31
- def self.arced(...) = Arced.build(...)
32
-
33
- private_class_method :arced
34
-
35
25
  # Core API
36
26
 
37
27
  # Returns a copy moved to a point and optional offset.
@@ -62,7 +52,12 @@ module Sevgi
62
52
  # @raise [Sevgi::PanicError] when a subclass does not implement box
63
53
  def box = PanicError.("#{self.class}#box must be implemented")
64
54
 
65
- # Returns equations that define the element boundary.
55
+ # Reports whether the element boundary forms a closed path.
56
+ # @return [Boolean]
57
+ def closed? = self.class.send(:close?)
58
+
59
+ # Returns carrier equations for candidate boundary intersections.
60
+ # A finite element can represent only part of each carrier.
66
61
  # @abstract Subclasses implement element-specific equations.
67
62
  # @return [Array<Sevgi::Geometry::Equation>]
68
63
  # @raise [Sevgi::PanicError] when a subclass does not implement equations
@@ -73,6 +68,29 @@ module Sevgi
73
68
  # @return [Boolean]
74
69
  def ignorable?(precision: nil) = F.zero?(box.width, precision:) && F.zero?(box.height, precision:)
75
70
 
71
+ # Intersects the element boundary with an equation.
72
+ #
73
+ # Precision controls boundary membership, returned-coordinate rounding, and duplicate collapse.
74
+ # A nil precision uses the current thread's function precision for all three stages.
75
+ # @example Intersect a rectangle with a vertical line
76
+ # rect = Sevgi::Geometry::Rect[8, 4]
77
+ # axis = Sevgi::Geometry::Equation.vertical(3)
78
+ # rect.intersection(axis).map(&:deconstruct) # => [[3.0, 0.0], [3.0, 4.0]]
79
+ # @param equation [Sevgi::Geometry::Equation] equation to intersect with
80
+ # @param precision [Integer, nil] coordinate precision, or nil for the current function default
81
+ # @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
82
+ # @raise [Sevgi::Geometry::Error] when equation is not an equation
83
+ # @raise [Sevgi::PanicError] when the equation combination is not implemented
84
+ def intersection(equation, precision: nil)
85
+ Error.("Must be an equation: #{equation}") unless equation.is_a?(Equation)
86
+
87
+ points = equations.flat_map do |candidate|
88
+ equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
89
+ end
90
+
91
+ points.map { |point| point.approx(precision) }.uniq
92
+ end
93
+
76
94
  # Returns the element position.
77
95
  # @abstract Subclasses implement element-specific positioning.
78
96
  # @return [Sevgi::Geometry::Point]
@@ -87,14 +105,24 @@ module Sevgi
87
105
  # @raise [Sevgi::PanicError] when a subclass does not implement translate
88
106
  def translate(_x, _y) = PanicError.("#{self.class}#translate must be implemented")
89
107
 
108
+ def boundary_point?(point, precision)
109
+ return on?(point) if precision.nil?
110
+
111
+ F.with_precision(precision) { on?(point) }
112
+ end
113
+
114
+ private :boundary_point?
115
+
90
116
  # Element whose boundary is represented by straight segments.
91
117
  #
92
118
  # 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
119
+ # {#lines} collections. {#vertices} omits the repeated closing point from
120
+ # closed shapes. Open paths have the same values in `vertices` and `points`.
121
+ # Only closed shapes have a filled interior, so `inside?` on an open path
122
+ # is equivalent to testing its boundary.
123
+ # @example Inspect path and geometric views
97
124
  # rect = Sevgi::Geometry::Rect[8, 4]
125
+ # rect.vertices.size # => 4
98
126
  # rect.points.size # => 5
99
127
  # rect.segments.size # => 4
100
128
  # rect.lines.size # => 4
@@ -103,6 +131,18 @@ module Sevgi
103
131
  # Open lined element base class.
104
132
  # @api private
105
133
  Open = Class.new(self) do
134
+ # Returns the first point in the directed path.
135
+ # @return [Sevgi::Geometry::Point]
136
+ def starting = points.first
137
+
138
+ # Returns the last point in the directed path.
139
+ # @return [Sevgi::Geometry::Point]
140
+ def ending = points.last
141
+
142
+ # Returns the same trace with opposite traversal.
143
+ # @return [Sevgi::Geometry::Element::Lined]
144
+ def reverse = self.class.send(:new_by_points!, *points.reverse)
145
+
106
146
  # Draws the element as an SVG polyline.
107
147
  # @param node [Object] graphics node receiving the drawing command
108
148
  # @return [Object] graphics node command result
@@ -313,13 +353,23 @@ module Sevgi
313
353
  approx.send(:draw!, ...)
314
354
  end
315
355
 
316
- # Returns immutable element points.
356
+ # Returns immutable element points in path order.
357
+ # Closed elements repeat the first vertex at the end.
317
358
  # @param approximate [Boolean] true to round points with the current function precision
318
359
  # @return [Array<Sevgi::Geometry::Point>] frozen point collection
319
360
  def points(approximate = false)
320
361
  approximate ? rounded_points(nil) : @points
321
362
  end
322
363
 
364
+ # Returns immutable geometric vertices in path order.
365
+ # Closed elements omit the repeated closing point. Open elements return {#points}.
366
+ # @return [Array<Sevgi::Geometry::Point>] frozen vertex collection
367
+ def vertices
368
+ return points unless closed?
369
+
370
+ @vertices ||= points[...-1].freeze
371
+ end
372
+
323
373
  # Returns the first point.
324
374
  # @return [Sevgi::Geometry::Point]
325
375
  def position
@@ -420,29 +470,6 @@ module Sevgi
420
470
  # @return [Array<Sevgi::Geometry::Equation::Linear>] frozen equation collection
421
471
  def equations = @equations ||= lines.map(&:equation).freeze
422
472
 
423
- # Intersects the element boundary with an equation.
424
- #
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]]
431
- # @param equation [Sevgi::Geometry::Equation] equation to intersect with
432
- # @param precision [Integer, nil] decimal precision for returned points, or nil for the current function default
433
- # @return [Array<Sevgi::Geometry::Point>] unique boundary intersection points
434
- # @raise [Sevgi::Geometry::Error] when equation is not an equation
435
- # @raise [Sevgi::PanicError] when the equation combination is not implemented
436
- def intersection(equation, precision: nil)
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
444
- end
445
-
446
473
  # Properties
447
474
 
448
475
  # Returns a line by index.
@@ -465,14 +492,19 @@ module Sevgi
465
492
  # @return [Sevgi::Geometry::Segment]
466
493
  def head = @head ||= segments.first
467
494
 
468
- # Returns immutable boundary lines derived from segments and points.
495
+ # Returns immutable boundary lines with the stored endpoints and segments.
469
496
  # @return [Array<Sevgi::Geometry::Line>] frozen line collection
470
497
  def lines
471
- @lines ||= segments
472
- .zip(points[...segments.size])
473
- .map { |segment, position|
474
- segment.line(position)
475
- }
498
+ @lines ||= points
499
+ .each_cons(2)
500
+ .zip(segments)
501
+ .map do |points, segment|
502
+ # Rebuilding from either view alone loses stored endpoints or input angles.
503
+ Line.send(:new) do
504
+ @points = points
505
+ @segments = [segment]
506
+ end
507
+ end
476
508
  .freeze
477
509
  end
478
510
 
@@ -488,7 +520,7 @@ module Sevgi
488
520
 
489
521
  # Reports whether a point is inside or on the boundary.
490
522
  #
491
- # Open paths have no filled interior; for them this predicate is true
523
+ # Open paths have no filled interior. For them this predicate is true
492
524
  # only for points on the actual path boundary.
493
525
  # @example Compare closed and open path containment
494
526
  # rect = Sevgi::Geometry::Rect[8, 4]
@@ -502,7 +534,7 @@ module Sevgi
502
534
  def inside?(point)
503
535
  point = Tuple[Point, point]
504
536
 
505
- return on?(point) unless self.class.send(:close?)
537
+ return on?(point) unless closed?
506
538
 
507
539
  on?(point) || pnpoly(points, point)
508
540
  end
@@ -525,18 +557,11 @@ module Sevgi
525
557
 
526
558
  private
527
559
 
528
- def boundary_point?(point, precision)
529
- return on?(point) if precision.nil?
530
-
531
- F.with_precision(precision) { on?(point) }
532
- end
533
-
534
560
  def calculate_points_from_segments
535
561
  Error.("No segments found") unless segments
536
562
 
537
563
  [point = position, *segments.map { point = it.ending(point) }].tap do |points|
538
- # Share the first point object when the path closes within the current precision.
539
- points[-1] = points.first if points.first.eq?(points.last)
564
+ points[-1] = points.first if closed? && points.first.eq?(points.last)
540
565
  end
541
566
  end
542
567
 
@@ -578,7 +603,7 @@ module Sevgi
578
603
 
579
604
  def rounded_points(precision)
580
605
  rounded = @points.map { it.approx(precision) }
581
- rounded[-1] = rounded.first if self.class.send(:close?)
606
+ rounded[-1] = rounded.first if closed?
582
607
  rounded.freeze
583
608
  end
584
609
 
@@ -592,7 +617,7 @@ module Sevgi
592
617
 
593
618
  Error.("Wrong number of points; expected #{np} where found #{points.size}") unless points.size == np
594
619
  Error.("Wrong number of segments; expected #{ns} where found #{segments.size}") unless segments.size == ns
595
- return unless self.class.send(:close?) && !points.first.eq?(points.last)
620
+ return unless closed? && !points.first.eq?(points.last)
596
621
 
597
622
  Error.("Element points must form a closed path")
598
623
  end
@@ -600,12 +625,38 @@ module Sevgi
600
625
  def validate_geometry! = nil
601
626
  end
602
627
 
603
- # Reserved base for future arced elements.
604
- # @api private
628
+ # Abstract family of circular and elliptical boundary elements.
629
+ # Arc is open, while Ellipse and Circle have closed boundaries. Concrete values share exact and precision-aware comparisons.
605
630
  class Arced < self
631
+ # Compares canonical fields with coordinate and numeric precision.
632
+ # @param other [Object] comparison target
633
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
634
+ # @return [Boolean]
635
+ def eq?(other, precision: nil)
636
+ other.instance_of?(self.class) &&
637
+ state.zip(other.send(:state)).all? do |left, right|
638
+ left.is_a?(::Numeric) ? F.eq?(left, right, precision:) : left.eq?(right, precision:)
639
+ end
640
+ end
641
+
642
+ # Reports strict equality by concrete class and canonical fields.
643
+ # @param other [Object] comparison target
644
+ # @return [Boolean]
645
+ def eql?(other) = other.instance_of?(self.class) && state == other.send(:state)
646
+
647
+ # Returns a hash independent of numeric precision.
648
+ # @return [Integer]
649
+ def hash = [self.class, *state].hash
650
+
651
+ # Reports whether a point is outside the element.
652
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
653
+ # @return [Boolean]
654
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
655
+ def outside?(point) = !inside?(point)
656
+
657
+ alias == eql?
606
658
  end
607
659
 
608
- private_constant :Arced
609
660
  end
610
661
 
611
662
  require_relative "elements/line"
@@ -614,5 +665,8 @@ module Sevgi
614
665
  require_relative "elements/polyline"
615
666
  require_relative "elements/rect"
616
667
  require_relative "elements/triangle"
668
+ require_relative "elements/ellipse"
669
+ require_relative "elements/circle"
670
+ require_relative "elements/arc"
617
671
  end
618
672
  end