sevgi-sundries 0.95.0 → 0.98.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dec50633b4f3c460af91dc1ab19341355f3ede4dd1df4bac99d4ab076b0ac53
4
- data.tar.gz: b6d478d2d796f1a6379f1a40301795b41f9dca9ad68cce017e17b2c369703641
3
+ metadata.gz: 7df3ccce8a77f50c584bd9ced3fec7fa19c3efb7c458d8dc248ca3629666fca3
4
+ data.tar.gz: 690408c4e42c34eb5fe500d405a6f2532c9b7f1b6350784d176ba77f40135ea8
5
5
  SHA512:
6
- metadata.gz: 9f36b8b5f8d7ca21602e67a50cc3272d452c4308a57b4773c94232337de9b236505e4283f09ffb85f57827087975b8ba53be8ee9d72b41b8f2d111ab6ec5ada3
7
- data.tar.gz: f82107592d7771d7ce72cbfc16276db3994097385a10205748064515110faba49b5c3fef842c405deb8b6f4746940736cb985f79be750ed9c58fdb674a014a7b
6
+ metadata.gz: c2c0a0062705f0c9dd55a0cef7093965a14cb9ce5e9f55ad9ca5f72485f715220fd8e9fdd616e707bf21eb3b198cb5011068aee9fff351c4e55e1aea5b03264a
7
+ data.tar.gz: 72ef749c915cc8782e313292a5da66960f5c50dce49cc071fa2492de97d2d948f3f28f05475290daf979e29247f9ae61a63e347077ceb1a923ec16746e7ddaad
data/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.98.2 - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
13
+
14
+ ## 0.98.1 - 2026-07-20
15
+
16
+ ### Changed
17
+
18
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
19
+
20
+ ### Removed
21
+
22
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
23
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
24
+
25
+ ## 0.98.0 - 2026-07-20
26
+
27
+ ### Added
28
+
29
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
30
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
31
+ destinations without changing physical load identity.
32
+
33
+ ## 0.97.0 - 2026-07-19
34
+
35
+ ### Added
36
+
37
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
38
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
39
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
40
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
41
+
42
+ ### Changed
43
+
44
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
45
+ application dependencies.
46
+
47
+ ## 0.96.0 - 2026-07-18
48
+
49
+ ### Added
50
+
51
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
52
+ `igves --omit` exposes the same behavior from the command line.
53
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
54
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
55
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
56
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
57
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
58
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
59
+
60
+ ### Changed
61
+
62
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
63
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
64
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
65
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
66
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
67
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
68
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
69
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
70
+ `constraint` replace axis-implying constructor names.
71
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
72
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
73
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
74
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
75
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
76
+ file inputs now use the corresponding `File`-suffixed entrypoints.
77
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
78
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
79
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
80
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
81
+ `Stay` for traversal stop tokens.
82
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
83
+ return nil from `<=>`.
84
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
85
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
86
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
87
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
88
+ elements, and Grid query values.
89
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
90
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
91
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
92
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
93
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
94
+ public state while parser, strategy, and construction plumbing remain private.
95
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
96
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
97
+
98
+ ### Fixed
99
+
100
+ - Rejected directories from default file discovery while preserving custom locator matchers.
101
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
102
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
103
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
104
+ tablet menu at 1024px and below.
105
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
106
+ arbitrary user-supplied attributes remain untouched.
107
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
108
+ rendered id value, including false and numeric values, through its serialized string.
109
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
110
+ and validated raw output paths before expansion or rendering.
111
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
112
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
113
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
114
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
115
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
116
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
117
+
118
+ ### Removed
119
+
120
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
121
+ immutable.
122
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
123
+ executables remain unchanged.
124
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
125
+ now keep the harness under the Showcase namespace.
126
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
127
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
128
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
129
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
130
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
131
+ bracket constructors, and direct Stop construction.
132
+
8
133
  ## 0.95.0 - 2026-07-11
9
134
 
10
135
  ### Security
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sevgi Sundries
2
2
 
3
- Helper objects and export tools for Sevgi drawings.
3
+ Sevgi Sundries contains shared layout objects and optional export tools.
4
4
 
5
5
  ## Install
6
6
 
@@ -24,7 +24,7 @@ tile.box.height
24
24
 
25
25
  ## Ruby compatibility
26
26
 
27
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
27
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
28
28
 
29
29
  ## Native prerequisites
30
30
 
@@ -49,7 +49,7 @@ gem install cairo rsvg2 hexapdf
49
49
 
50
50
  ## Links
51
51
 
52
- - Documentation: https://sevgi.roktas.dev
53
- - API documentation: https://www.rubydoc.info/gems/sevgi-sundries
54
- - Source: https://github.com/roktas/sevgi/tree/main/sundries
55
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
52
+ - Documentation: <https://sevgi.roktas.dev>
53
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-sundries>
54
+ - Source: <https://github.com/roktas/sevgi/tree/main/sundries>
55
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
@@ -20,29 +20,32 @@ module Sevgi
20
20
 
21
21
  # Exports SVG source to a PDF or PNG file using librsvg and Cairo.
22
22
  # @param svg [String] SVG source content
23
- # @param output [String, #to_s] output file path
23
+ # Relative paths are expanded, missing parent directories are created after all render inputs validate, and an
24
+ # existing output file is replaced. Directory paths are not expanded to a default file name.
25
+ # @param output [String, #to_path] output file path
24
26
  # @param format [Symbol, String, nil] explicit output format, or nil to infer from output extension
25
27
  # @param width [Numeric, nil] finite positive target width in output pixels for PNG, or CSS pixels before PDF point conversion
26
28
  # @param height [Numeric, nil] finite positive target height in output pixels for PNG, or CSS pixels before PDF point conversion
27
- # @param dpi [Numeric] finite positive CSS pixel density used for absolute SVG units and PDF point conversion
29
+ # @param dpi [Numeric] finite positive CSS pixel density; omission uses {DEFAULT_DPI}, but explicit nil is invalid
28
30
  # @param css [String, nil] CSS inserted before the closing svg tag before rendering
29
31
  # @yield [svg] optional source transformation applied before rendering
30
32
  # @yieldparam svg [String] SVG source after optional CSS injection
31
33
  # @yieldreturn [String] SVG source to render
32
- # @return [Object] the original output argument
33
- # @raise [Sevgi::ArgumentError] when output, CSS, or transformed SVG has an invalid type
34
- # @raise [Sevgi::Sundries::Export::ExportError] when format, numeric options, SVG parsing, SVG dimensions, or render dimensions are invalid
34
+ # @return [String] expanded output path
35
+ # @raise [Sevgi::ArgumentError] when output is blank, invalid, or a directory, or CSS/transformed SVG has an
36
+ # invalid type
37
+ # @raise [Sevgi::Sundries::Export::ExportError] when format, numeric options, CSS insertion, SVG parsing, SVG dimensions, or render dimensions are invalid
38
+ # @raise [SystemCallError] when the output directory or file cannot be created or written
35
39
  def call(svg, output, format: nil, width: nil, height: nil, dpi: DEFAULT_DPI, css: nil, &block)
36
40
  ArgumentError.("SVG content must be a String") unless svg.is_a?(String)
37
- original_output = output
38
41
  output = output_path(output)
39
- format = format_for!(format, output)
42
+ format = format_for(format, output)
40
43
  width = dimension(width, "width")
41
44
  height = dimension(height, "height")
42
- dpi = dimension(dpi, "dpi")
45
+ dpi = dimension(dpi, "dpi", optional: false)
43
46
  ArgumentError.("Export CSS must be a String") unless css.nil? || css.is_a?(String)
44
47
 
45
- svg = inject(svg, css) if css && !css.strip.empty?
48
+ svg = styled(svg, css) if css && !css.strip.empty?
46
49
  svg = block.call(svg) if block
47
50
  ArgumentError.("SVG content must be a String") unless svg.is_a?(String)
48
51
 
@@ -62,6 +65,7 @@ module Sevgi
62
65
  tw, th = target_size(iw, ih, width, height)
63
66
  ExportError.("Invalid export dimensions") unless target_size?(format, tw, th)
64
67
 
68
+ ::FileUtils.mkdir_p(::File.dirname(output))
65
69
  renderer.call(
66
70
  handle: handle,
67
71
  output: output,
@@ -75,7 +79,7 @@ module Sevgi
75
79
  ExportError.("Render error: #{e.message}")
76
80
  end
77
81
 
78
- original_output
82
+ output
79
83
  end
80
84
 
81
85
  # Replaces exact placeholder text objects in PDF streams.
@@ -308,19 +312,21 @@ module Sevgi
308
312
  def output_path(output)
309
313
  ArgumentError.("Export output must be provided") if output.nil?
310
314
 
311
- path = output.to_s
312
- ArgumentError.("Export output must be a String-like path") unless path.is_a?(::String)
315
+ path = output.respond_to?(:to_path) ? output.to_path : output
316
+ ArgumentError.("Export output must be a String or path-like object") unless path.is_a?(::String)
313
317
  ArgumentError.("Export output must be provided") if path.strip.empty?
314
318
 
319
+ path = ::File.expand_path(path)
320
+ ArgumentError.("Export output must name a file") if ::File.directory?(path)
315
321
  path
316
322
  rescue ::StandardError => e
317
323
  raise if e.is_a?(::Sevgi::ArgumentError)
318
324
 
319
- ArgumentError.("Export output must be a String-like path: #{e.message}")
325
+ ArgumentError.("Export output must be a String or path-like object: #{e.message}")
320
326
  end
321
327
 
322
- def dimension(value, field)
323
- return if value.nil?
328
+ def dimension(value, field, optional: true)
329
+ return if value.nil? && optional
324
330
  ExportError.(dimension_error(field)) unless value.is_a?(::Numeric)
325
331
 
326
332
  number = begin
@@ -40,7 +40,7 @@ module Sevgi
40
40
  # @param page [Integer, String, nil] page selector passed to Inkscape
41
41
  # @param css [String, nil] CSS inserted before exporting
42
42
  # @return [Sevgi::Function::Shell::Result] command result
43
- # @raise [Sevgi::Sundries::Export::ExportError] when format or output extension is unsupported
43
+ # @raise [Sevgi::Sundries::Export::ExportError] when format, output extension, or CSS insertion is invalid
44
44
  # @raise [Sevgi::Error] when Inkscape is missing or the command fails
45
45
  # @raise [Errno::ENOENT] when the executable cannot be spawned
46
46
  # @see https://inkscape.org/ Inkscape
@@ -59,11 +59,11 @@ module Sevgi
59
59
  infile = File.expand_path(infile)
60
60
  outfile ||= F.subext(".pdf", infile)
61
61
  outfile = File.expand_path(outfile)
62
- format = format_for!(format, outfile)
62
+ format = format_for(format, outfile)
63
63
 
64
64
  if css
65
65
  temp = Tempfile.new(%w[input .svg], File.dirname(infile))
66
- ::File.write(temp.path, inject(::File.read(infile), css))
66
+ ::File.write(temp.path, styled(::File.read(infile), css))
67
67
  infile = temp.path
68
68
  end
69
69
 
@@ -98,7 +98,7 @@ module Sevgi
98
98
  # @param id [String, nil] SVG element id to export
99
99
  # @param css [String, nil] CSS inserted before exporting
100
100
  # @return [Sevgi::Function::Shell::Result] command result
101
- # @raise [Sevgi::Sundries::Export::ExportError] when format or output extension is unsupported
101
+ # @raise [Sevgi::Sundries::Export::ExportError] when format, output extension, or CSS insertion is invalid
102
102
  # @raise [Sevgi::Error] when rsvg-convert is missing or the command fails
103
103
  # @raise [Errno::ENOENT] when the executable cannot be spawned
104
104
  # @see https://gitlab.gnome.org/GNOME/librsvg librsvg
@@ -115,11 +115,11 @@ module Sevgi
115
115
  infile = File.expand_path(infile)
116
116
  outfile ||= F.subext(".pdf", infile)
117
117
  outfile = File.expand_path(outfile)
118
- format = format_for!(format, outfile)
118
+ format = format_for(format, outfile)
119
119
 
120
120
  if css
121
121
  temp = Tempfile.new(%w[input .svg], File.dirname(infile))
122
- ::File.write(temp.path, inject(::File.read(infile), css))
122
+ ::File.write(temp.path, styled(::File.read(infile), css))
123
123
  infile = temp.path
124
124
  end
125
125
 
@@ -6,19 +6,32 @@ require_relative "export/system"
6
6
 
7
7
  module Sevgi
8
8
  module Sundries
9
- # Exports SVG content and post-processes PDF output.
9
+ # Exports SVG source to PNG or PDF and post-processes PDF output.
10
10
  #
11
11
  # Native PDF/PNG rendering is loaded lazily so installing `sevgi-sundries` for SVG-only helpers does not require the
12
12
  # Cairo, RSVG, or HexaPDF gems. Native export entrypoints raise {Sevgi::MissingComponentError} when those optional
13
- # gems are unavailable.
13
+ # gems are unavailable. Omit `format:` to infer it from the output suffix;
14
+ # width and height are output dimensions rather than changes to the SVG
15
+ # viewBox. The return value is the expanded path that was written.
16
+ #
17
+ # @example Export SVG source to a sized PNG
18
+ # svg = Sevgi::Graphics.SVG(:minimal) { circle cx: 5, cy: 5, r: 4 }.Render
19
+ # Sevgi::Sundries::Export.call(svg, "drawing.png", width: 320)
20
+ # @example Infer PDF output and inject export-only CSS
21
+ # svg = Sevgi::Graphics.SVG(:minimal) { circle class: "accent", r: 4 }.Render
22
+ # Sevgi::Sundries::Export.call(svg, "drawing.pdf", css: ".accent { fill: tomato; }")
23
+ # @see https://sevgi.roktas.dev/sundries/#export Export guide
14
24
  module Export
15
- # Supported export format names mapped to file extensions.
16
- AVAILABLE = (EXTENSIONS = {
25
+ # File extensions mapped to export format names.
26
+ # @api private
27
+ EXTENSIONS = {
17
28
  ".pdf" => :pdf,
18
29
  ".png" => :png
19
- }.freeze)
20
- .invert
21
- .freeze
30
+ }.freeze
31
+ private_constant :EXTENSIONS
32
+
33
+ # Supported export format names mapped to file extensions.
34
+ AVAILABLE = EXTENSIONS.invert.freeze
22
35
 
23
36
  # Default SVG CSS pixel density.
24
37
  DEFAULT_DPI = 96.0
@@ -37,28 +50,26 @@ module Sevgi
37
50
  # @overload call(svg, output, format: nil, width: nil, height: nil, dpi: DEFAULT_DPI, css: nil)
38
51
  # Exports SVG source to a PDF or PNG file using the optional native export gems.
39
52
  # @param svg [String] SVG source content
40
- # @param output [String, #to_s] output file path
53
+ # Relative paths are expanded, missing parent directories are created after all render inputs validate, and an
54
+ # existing output file is replaced. Directory paths are not expanded to a default file name.
55
+ # @param output [String, #to_path] output file path
41
56
  # @param format [Symbol, String, nil] explicit output format, or nil to infer from output extension
42
57
  # @param width [Numeric, nil] target width in output pixels for PNG, or CSS pixels before PDF point conversion
43
58
  # @param height [Numeric, nil] target height in output pixels for PNG, or CSS pixels before PDF point conversion
44
- # @param dpi [Numeric] CSS pixel density used for absolute SVG units and PDF point conversion
59
+ # @param dpi [Numeric] finite positive CSS pixel density; omission uses {DEFAULT_DPI}, but explicit nil is invalid
45
60
  # @param css [String, nil] CSS inserted before the closing svg tag before rendering
46
61
  # @yield [svg] optional source transformation applied before rendering
47
62
  # @yieldparam svg [String] SVG source after optional CSS injection
48
63
  # @yieldreturn [String] SVG source to render
49
- # @return [Object] the original output argument
50
- # @raise [Sevgi::ArgumentError] when SVG content is not a string or output is blank
64
+ # @return [String] expanded output path
65
+ # @raise [Sevgi::ArgumentError] when SVG content is not a string or output is blank, invalid, or a directory
51
66
  # @raise [Sevgi::MissingComponentError] when cairo, hexapdf, or rsvg2 is unavailable
52
- # @raise [Sevgi::Sundries::Export::ExportError] when format, SVG parsing, SVG dimensions, or render dimensions
53
- # are invalid
67
+ # @raise [Sevgi::Sundries::Export::ExportError] when format, CSS insertion, SVG parsing, SVG dimensions, or
68
+ # render dimensions are invalid
69
+ # @raise [SystemCallError] when the output directory or file cannot be created or written
54
70
  def call(*args, **kwargs, &block) = native!.call(*args, **kwargs, &block)
55
71
 
56
- # Resolves the export format from an explicit value or output extension.
57
- # @param format [Symbol, String, nil] explicit format
58
- # @param output [String, #to_s] output path
59
- # @return [Symbol] resolved format
60
- # @raise [Sevgi::Sundries::Export::ExportError] when the explicit format or output extension is unsupported
61
- def format_for!(format, output)
72
+ def format_for(format, output)
62
73
  if format
63
74
  format = normalize_format(format)
64
75
  ExportError.("Unsupported export format: #{format}") unless AVAILABLE.key?(format)
@@ -72,12 +83,12 @@ module Sevgi
72
83
  end
73
84
  end
74
85
 
75
- # Inserts CSS before the closing svg tag.
76
- # @param svg [String] SVG source content
77
- # @param css [String] CSS source content
78
- # @return [String] SVG source with an added style element when a closing svg tag is present
79
- # @raise [NoMethodError] when SVG content does not support string substitution
80
- def inject(svg, css) = svg.sub("</svg>", "<style>#{css}</style></svg>")
86
+ def styled(svg, css)
87
+ output = svg.sub("</svg>", "<style>#{css}</style></svg>")
88
+ ExportError.("Cannot insert CSS: closing svg tag not found") if output == svg
89
+
90
+ output
91
+ end
81
92
 
82
93
  def normalize_format(format)
83
94
  unless format.is_a?(::String) || format.is_a?(::Symbol)
@@ -87,7 +98,7 @@ module Sevgi
87
98
  format.to_sym
88
99
  end
89
100
 
90
- private :normalize_format
101
+ private :format_for, :normalize_format, :styled
91
102
 
92
103
  # Replaces exact placeholder text objects in PDF streams.
93
104
  # @param infile [String] source PDF file path
@@ -5,6 +5,34 @@ require "delegate"
5
5
  module Sevgi
6
6
  module Sundries
7
7
  # Builds a tile-like grid from horizontal and vertical rulers.
8
+ #
9
+ # Axis names describe line direction, not the coordinate used to place a
10
+ # line: `grid.x` produces horizontal lines whose y positions come from the
11
+ # vertical ruler; `grid.y` produces vertical lines whose x positions come
12
+ # from the horizontal ruler. Each query can return geometry lines, Point
13
+ # endpoint pairs, or plain coordinate pairs for different consumers.
14
+ # @example Query fitted horizontal and vertical lines
15
+ # x = Sevgi::Sundries::Ruler.new(brut: 80, unit: 1, multiple: 10, margins: [5])
16
+ # y = Sevgi::Sundries::Ruler.new(brut: 50, unit: 1, multiple: 10, margins: [5])
17
+ # grid = Sevgi::Sundries::Grid[x, y]
18
+ # grid.x.major.lines.size # => 5
19
+ # grid.y.minor.lines.size # => 71
20
+ # @example Preserve a source canvas while fitting axis margins
21
+ # canvas = Sevgi::Graphics::Canvas.call(width: 80, height: 50)
22
+ # x = Sevgi::Sundries::Ruler.new(brut: 80, unit: 1, multiple: 10, margins: [5, 7])
23
+ # y = Sevgi::Sundries::Ruler.new(brut: 50, unit: 1, multiple: 10, margins: [3, 5])
24
+ # grid = Sevgi::Sundries::Grid.new(x:, y:, canvas:)
25
+ # grid.canvas.margin.to_a # => [4.0, 11.0, 6.0, 9.0]
26
+ # @example Use lines, points, coordinate pairs, and inherited row boxes
27
+ # x = Sevgi::Sundries::Ruler.new(brut: 30, unit: 1, multiple: 10)
28
+ # y = Sevgi::Sundries::Ruler.new(brut: 20, unit: 1, multiple: 10)
29
+ # grid = Sevgi::Sundries::Grid[x, y]
30
+ # grid.x.major.lines.size # => 3
31
+ # grid.x.major.points.first.map(&:deconstruct) # => [[0.0, 0.0], [30.0, 0.0]]
32
+ # grid.y.halve.xys.first # => [[5.0, 0.0], [5.0, 20.0]]
33
+ # grid.rowbox.approx.height # => 10.0
34
+ # @see Sevgi::Sundries::Ruler
35
+ # @see Sevgi::Graphics::Mixtures::Hatch
8
36
  class Grid < Tile
9
37
  # Builds a grid using bracket syntax.
10
38
  # @param x [Sevgi::Sundries::Ruler] horizontal ruler
@@ -13,34 +41,55 @@ module Sevgi
13
41
  # @raise [Sevgi::ArgumentError] when either argument is not a ruler
14
42
  def self.[](x, y) = new(x:, y:)
15
43
 
16
- # Returns the horizontal axis ruler and line queries.
44
+ # Returns the horizontal-line axis and its ruler queries.
45
+ # Line positions are supplied by the vertical ruler.
17
46
  # @return [Sevgi::Sundries::Grid::X]
18
47
  attr_reader :x
19
48
 
20
- # Returns the vertical axis ruler and line queries.
49
+ # Returns the vertical-line axis and its ruler queries.
50
+ # Line positions are supplied by the horizontal ruler.
21
51
  # @return [Sevgi::Sundries::Grid::Y]
22
52
  attr_reader :y
23
53
 
24
54
  # Creates a grid from horizontal and vertical rulers.
25
55
  # @param x [Sevgi::Sundries::Ruler] horizontal ruler
26
56
  # @param y [Sevgi::Sundries::Ruler] vertical ruler
57
+ # @param canvas [Sevgi::Graphics::Canvas, nil] source canvas whose identity should be preserved
27
58
  # @return [void]
28
59
  # @raise [Sevgi::ArgumentError] when either argument is not a ruler
29
- def initialize(x:, y:)
30
- ArgumentError.("Arguments must be Ruler objects") unless [x, y].all?(Ruler)
60
+ # @raise [Sevgi::ArgumentError] when the source canvas does not match the ruler spans
61
+ # @raise [Sevgi::ArgumentError] when either ruler fits no interval
62
+ def initialize(x:, y:, canvas: nil)
63
+ validate_axes(x, y)
64
+ validate_canvas(canvas, x, y)
31
65
 
32
- @x = X.new(x, y)
33
- @y = Y.new(x, y)
66
+ @source = canvas
67
+ @x = X.send(:new, x, y)
68
+ @y = Y.send(:new, x, y)
34
69
 
35
70
  super(Geometry::Rect[@x.u, @y.u], nx: @x.n, ny: @y.n)
36
71
  end
37
72
 
38
- # Returns a graphics canvas matching the rulers and computed margins.
73
+ # Returns a graphics canvas matching the ruler spans and fitted margins.
74
+ # Horizontal ruler margins become the canvas left/right margins; vertical
75
+ # ruler margins become its top/bottom margins.
76
+ # @example Build a drawing with the fitted canvas
77
+ # x = Sevgi::Sundries::Ruler.new(brut: 80, unit: 1, multiple: 10, margins: [5])
78
+ # y = Sevgi::Sundries::Ruler.new(brut: 50, unit: 1, multiple: 10, margins: [5])
79
+ # grid = Sevgi::Sundries::Grid[x, y]
80
+ # drawing = Sevgi::Graphics.SVG(:inkscape, grid.canvas) do
81
+ # Draw grid.x.major.lines, class: %w[guide horizontal]
82
+ # Draw grid.y.major.lines, class: %w[guide vertical]
83
+ # end
84
+ # drawing.Render
39
85
  # @return [Sevgi::Graphics::Canvas]
40
86
  def canvas
87
+ margins = [y.start, x.finish, y.finish, x.start]
88
+ return @source.with(margins:) if @source
89
+
41
90
  Graphics::Canvas.new(
42
91
  **Graphics::Paper[x.brut, y.brut].to_h,
43
- margins: Graphics::Margin[y.margin, x.margin].to_a
92
+ margins:
44
93
  )
45
94
  end
46
95
 
@@ -52,17 +101,19 @@ module Sevgi
52
101
  # @return [Float]
53
102
  def width = x.d
54
103
 
55
- # Axis wrapper exposing grid line queries for one ruler direction.
104
+ # Axis wrapper exposing grid line queries for one line direction.
56
105
  class Axis < DelegateClass(Ruler)
57
- # Returns the major line query.
106
+ private_class_method :new
107
+
108
+ # Returns lines placed at the perpendicular ruler's major distances.
58
109
  # @return [Sevgi::Sundries::Grid::Axis::Major]
59
110
  attr_reader :major
60
111
 
61
- # Returns the midpoint line query.
112
+ # Returns lines placed at the perpendicular ruler's halfway distances.
62
113
  # @return [Sevgi::Sundries::Grid::Axis::Halve]
63
114
  attr_reader :halve
64
115
 
65
- # Returns the minor line query.
116
+ # Returns lines placed at the perpendicular ruler's minor distances.
66
117
  # @return [Sevgi::Sundries::Grid::Axis::Minor]
67
118
  attr_reader :minor
68
119
 
@@ -70,20 +121,24 @@ module Sevgi
70
121
  # @param this [Sevgi::Sundries::Ruler] ruler represented by this axis
71
122
  # @param other [Sevgi::Sundries::Ruler] ruler used for perpendicular tick locations
72
123
  # @return [void]
124
+ # @api private
73
125
  def initialize(this, other)
74
126
  super(this)
75
127
 
76
- @major = Major.new(self, other)
77
- @halve = Halve.new(self, other)
78
- @minor = Minor.new(self, other)
128
+ @major = Major.send(:new, self, other)
129
+ @halve = Halve.send(:new, self, other)
130
+ @minor = Minor.send(:new, self, other)
79
131
  end
80
132
 
81
133
  # Memoized grid line query for an axis.
82
134
  class Query
135
+ private_class_method :new
136
+
83
137
  # Creates a query.
84
138
  # @param this [Sevgi::Sundries::Grid::Axis] axis receiving generated lines
85
139
  # @param other [Sevgi::Sundries::Ruler] perpendicular ruler supplying tick distances
86
140
  # @return [void]
141
+ # @api private
87
142
  def initialize(this, other) = (@this, @other = this, other)
88
143
 
89
144
  # Returns grid line endpoints as coordinate pairs.
@@ -99,7 +154,7 @@ module Sevgi
99
154
  # Returns generated grid lines.
100
155
  # The memoized collection is frozen and must be treated as immutable.
101
156
  # @return [Array<Sevgi::Geometry::Line>] frozen lines
102
- def lines = @lines ||= lines!.freeze
157
+ def lines = @lines ||= build.freeze
103
158
 
104
159
  private
105
160
 
@@ -108,23 +163,23 @@ module Sevgi
108
163
 
109
164
  # Major grid line query.
110
165
  class Major < Query
111
- # Returns lines at major tick distances.
112
- # @return [Array<Sevgi::Geometry::Line>]
113
- def lines! = other.ds.map { this.line_at(it) }
166
+ private
167
+
168
+ def build = other.ds.map { this.line_at(it) }
114
169
  end
115
170
 
116
171
  # Midpoint grid line query.
117
172
  class Halve < Query
118
- # Returns lines at midpoint tick distances.
119
- # @return [Array<Sevgi::Geometry::Line>]
120
- def lines! = other.hs.map { this.line_at(it) }
173
+ private
174
+
175
+ def build = other.hs.map { this.line_at(it) }
121
176
  end
122
177
 
123
178
  # Minor grid line query.
124
179
  class Minor < Query
125
- # Returns lines at minor tick distances.
126
- # @return [Array<Sevgi::Geometry::Line>]
127
- def lines! = other.ms.map { this.line_at(it) }
180
+ private
181
+
182
+ def build = other.ms.map { this.line_at(it) }
128
183
  end
129
184
  end
130
185
 
@@ -146,6 +201,7 @@ module Sevgi
146
201
  # @param this [Sevgi::Sundries::Ruler] horizontal ruler
147
202
  # @param other [Sevgi::Sundries::Ruler] vertical ruler
148
203
  # @return [void]
204
+ # @api private
149
205
  def initialize(this, other) = super(other, this)
150
206
 
151
207
  # Returns the base vertical line for this axis.
@@ -157,6 +213,22 @@ module Sevgi
157
213
  # @return [Sevgi::Geometry::Line]
158
214
  def line_at(x) = line.at([x, 0.0])
159
215
  end
216
+
217
+ private
218
+
219
+ def validate_axes(x, y)
220
+ ArgumentError.("Arguments must be Ruler objects") unless [x, y].all?(Ruler)
221
+ ArgumentError.("Grid rulers must fit at least one interval") unless x.n.positive? && y.n.positive?
222
+ end
223
+
224
+ def validate_canvas(canvas, x, y)
225
+ return unless canvas
226
+
227
+ ArgumentError.("Grid canvas must match the ruler spans") unless canvas.is_a?(Graphics::Canvas)
228
+ return if canvas.width == x.brut && canvas.height == y.brut
229
+
230
+ ArgumentError.("Grid canvas must match the ruler spans")
231
+ end
160
232
  end
161
233
  end
162
234
  end
@@ -5,15 +5,20 @@ module Sevgi
5
5
  # A one-dimensional interval divided into equal units.
6
6
  #
7
7
  # The compact reader names are part of the domain vocabulary:
8
- # `u` is the unit length, `n` is the interval count, `d` is total
9
- # distance, and `h` is the midpoint distance.
8
+ # `u` is the unit length, `n` is the interval count, and `d` is total
9
+ # distance. `ds` includes both endpoints, while `hs` contains one halfway
10
+ # distance per interval. These compact names are intended to read as
11
+ # formulas in layout code rather than as general-purpose collection names.
10
12
  #
11
13
  # @example Interval geometry
12
- # # <---------------- d = n x u ---------------->
13
- # # |---------+---------+---------+---------|
14
- # # <--- u --->
15
14
  # interval = Sevgi::Sundries::Interval[3, 4]
15
+ # interval.u # => 3.0
16
+ # interval.n # => 4
16
17
  # interval.d # => 12.0
18
+ # @example Query major and halfway distances
19
+ # interval = Sevgi::Sundries::Interval[3, 4]
20
+ # interval.ds # => [0.0, 3.0, 6.0, 9.0, 12.0]
21
+ # interval.hs # => [1.5, 4.5, 7.5, 10.5]
17
22
  class Interval
18
23
  # Builds an interval using bracket syntax.
19
24
  # @param e [Numeric, #length] unit length or an object exposing length
@@ -149,21 +154,42 @@ module Sevgi
149
154
  # Fits a repeated interval into a broader span with computed margins.
150
155
  #
151
156
  # A ruler stores both the fitted major interval and the source subinterval.
152
- # The compact reader names mirror {Interval}: `brut` is the full available
153
- # span, `sd/su/sn` describe the subinterval, and `waste` is distributed as
154
- # equal margins.
157
+ # `brut` is the full available span. `unit * multiple` becomes the major
158
+ # interval, and `sd/su/sn` describe that source subinterval. Requested
159
+ # margins are minimums: leftover space is split between them while their
160
+ # start/end difference is preserved. The fitted `d` excludes those margins;
161
+ # `waste` includes them and any remainder.
155
162
  #
156
163
  # @example Ruler geometry
157
- # # <--------- d = n x sd ---------><--- waste = 2 x margin --->
158
- # # <----- u = unit x multiple ----->
159
- # # <---------------- brut ---------------->
160
164
  # ruler = Sevgi::Sundries::Ruler.new(unit: 1, multiple: 10, brut: 150)
161
- # ruler.d # => 150.0
165
+ # ruler.su # => 1.0
166
+ # ruler.sn # => 10
167
+ # ruler.u # => 10.0
168
+ # ruler.n # => 15
169
+ # ruler.d # => 150.0
170
+ # @example Fit whole major intervals inside minimum margins
171
+ # ruler = Sevgi::Sundries::Ruler.new(brut: 103, unit: 1, multiple: 10, margins: [5])
172
+ # ruler.n # => 9
173
+ # ruler.margins # => [6.5, 6.5]
174
+ # ruler.waste # => 13.0
175
+ # @example Preserve an asymmetric margin difference
176
+ # ruler = Sevgi::Sundries::Ruler.new(brut: 100, unit: 1, multiple: 10, margins: [5, 15])
177
+ # ruler.margins # => [5.0, 15.0]
178
+ # ruler.ds.last # => 80.0
179
+ # @see Sevgi::Sundries::Grid
162
180
  class Ruler < Interval
163
181
  # Returns the full available span before fitting.
164
182
  # @return [Float]
165
183
  attr_reader :brut
166
184
 
185
+ # Returns the fitted margin after the interval.
186
+ # @return [Float]
187
+ attr_reader :finish
188
+
189
+ # Returns the fitted margin before the interval.
190
+ # @return [Float]
191
+ attr_reader :start
192
+
167
193
  # Returns the source subinterval.
168
194
  # @return [Sevgi::Sundries::Interval]
169
195
  attr_reader :sub
@@ -172,7 +198,7 @@ module Sevgi
172
198
  # @param brut [Numeric] full available span
173
199
  # @param unit [Numeric] subinterval unit length
174
200
  # @param multiple [Integer] number of subinterval units per major interval
175
- # @param margin [Numeric] minimum margin on each side
201
+ # @param margins [Array<Numeric>] one symmetric or two start/end minimum margins
176
202
  # @return [void]
177
203
  # @raise [Sevgi::ArgumentError] when brut is not numeric
178
204
  # @raise [Sevgi::ArgumentError] when brut is not finite
@@ -181,31 +207,35 @@ module Sevgi
181
207
  # @raise [Sevgi::ArgumentError] when unit is not finite
182
208
  # @raise [Sevgi::ArgumentError] when unit is not positive
183
209
  # @raise [Sevgi::ArgumentError] when multiple is not a positive integer
184
- # @raise [Sevgi::ArgumentError] when margin is not numeric
185
- # @raise [Sevgi::ArgumentError] when margin is not finite
186
- # @raise [Sevgi::ArgumentError] when margin is negative
210
+ # @raise [Sevgi::ArgumentError] when margins does not contain one or two values
211
+ # @raise [Sevgi::ArgumentError] when a margin is not numeric
212
+ # @raise [Sevgi::ArgumentError] when a margin is not finite
213
+ # @raise [Sevgi::ArgumentError] when a margin is negative
187
214
  # @raise [Sevgi::ArgumentError] when the fitting span is negative
188
- def initialize(brut:, unit:, multiple:, margin: 0.0)
215
+ def initialize(brut:, unit:, multiple:, margins: [0.0])
189
216
  @brut = non_negative_number(brut, "Ruler brut")
190
217
  unit = positive_number(unit, "Ruler unit")
191
218
  multiple = positive_integer(multiple, "Ruler multiple")
192
- margin = non_negative_number(margin, "Ruler margin")
193
219
  @sub = Interval.new(unit, multiple)
220
+ span, start, finish = fitting_span(margins)
194
221
 
195
- n = divide(unit:, multiple:, brut: @brut, margin:)
196
-
197
- ArgumentError.("Ruler fitting span must not be negative") if n.negative?
222
+ n = divide(unit:, multiple:, span:)
198
223
 
199
224
  super(@sub, n)
225
+ @start, @finish = fitted_margins(start, finish, span)
200
226
  end
201
227
 
202
228
  # Returns a ruler where the source subinterval is flattened into units.
229
+ # @example Expand major intervals into individual units
230
+ # ruler = Sevgi::Sundries::Ruler.new(brut: 103, unit: 1, multiple: 10, margins: [5])
231
+ # ruler.expand.n # => 90
232
+ # ruler.ms.size # => 91
203
233
  # @return [Sevgi::Sundries::Ruler]
204
- def expand = self.class.new(unit: sub.u, multiple: 1, brut: d + waste, margin:)
234
+ def expand = self.class.new(unit: sub.u, multiple: 1, brut: d + waste, margins:)
205
235
 
206
- # Returns the computed margin after fitting.
207
- # @return [Float]
208
- def margin = @margin ||= waste / 2.0
236
+ # Returns fitted start and finish margins.
237
+ # @return [Array<Float>] frozen margin pair
238
+ def margins = @margins ||= [start, finish].freeze
209
239
 
210
240
  # Returns minor tick distances across the fitted span.
211
241
  # The memoized collection is frozen and must be treated as immutable.
@@ -228,18 +258,48 @@ module Sevgi
228
258
  # @return [Float]
229
259
  def su = @sub.u
230
260
 
261
+ private
262
+
263
+ def fitted_margins(start, finish, span)
264
+ extra = (span - d) / 2.0
265
+ [start + extra, finish + extra]
266
+ end
267
+
268
+ def fitting_span(margins)
269
+ start, finish = margin_pair(margins)
270
+ span = brut - start - finish
271
+ ArgumentError.("Ruler fitting span must not be negative") if span.negative?
272
+
273
+ [span, start, finish]
274
+ end
275
+
276
+ def margin_pair(values)
277
+ unless values.is_a?(::Array) && [1, 2].include?(values.size)
278
+ ArgumentError.("Ruler margins must contain one or two values")
279
+ end
280
+
281
+ values = [values.first, values.first] if values.one?
282
+ values.map { non_negative_number(it, "Ruler margin") }
283
+ end
284
+
231
285
  protected
232
286
 
233
287
  # Computes the number of major intervals fitting in the available span.
234
288
  # @param unit [Numeric] subinterval unit length
235
289
  # @param multiple [Integer] number of subinterval units per major interval
236
- # @param brut [Numeric] full available span
237
- # @param margin [Numeric] minimum margin on each side
290
+ # @param span [Numeric] span available after margins
238
291
  # @return [Integer]
239
- def divide(unit:, multiple:, brut:, margin:) = F.count(brut - (2 * margin), unit * multiple)
292
+ def divide(unit:, multiple:, span:) = F.count(span, unit * multiple)
240
293
  end
241
294
 
242
295
  # Ruler variant that always chooses an even number of major intervals.
296
+ #
297
+ # If ordinary fitting produces an odd count, one complete major interval is
298
+ # removed and the additional space is distributed through the margins.
299
+ # @example Reserve symmetric waste when an odd count would fit
300
+ # ruler = Sevgi::Sundries::RulerEven.new(brut: 50, unit: 1, multiple: 10)
301
+ # ruler.n # => 4
302
+ # ruler.margins # => [5.0, 5.0]
243
303
  class RulerEven < Ruler
244
304
  protected
245
305
 
@@ -3,6 +3,25 @@
3
3
  module Sevgi
4
4
  module Sundries
5
5
  # Repeats a geometry element over a rectangular row and column layout.
6
+ #
7
+ # The source element's bounding-box width and height become the cell pitch.
8
+ # Indexing is row-first: `tile[row][column]`. Generated cells and collection
9
+ # snapshots are immutable geometry values; no SVG elements are created.
10
+ # @example Address cells by row and column
11
+ # cell = Sevgi::Geometry::Rect[8, 4]
12
+ # tile = Sevgi::Sundries::Tile.new(cell, position: [10, 20], nx: 3, ny: 2)
13
+ # tile[1][2].position.deconstruct # => [26.0, 24.0]
14
+ # @example Inspect the complete, row, and column bounds
15
+ # cell = Sevgi::Geometry::Rect[8, 4]
16
+ # tile = Sevgi::Sundries::Tile.new(cell, position: [10, 20], nx: 3, ny: 2)
17
+ # [tile.box.approx.width, tile.box.approx.height] # => [24.0, 8.0]
18
+ # tile.rowbox(1).position.deconstruct # => [10.0, 24.0]
19
+ # tile.colbox(2).position.deconstruct # => [26.0, 20.0]
20
+ # @example Iterate by rows or columns
21
+ # tile = Sevgi::Sundries::Tile.new(Sevgi::Geometry::Rect[8, 4], nx: 3, ny: 2)
22
+ # tile.each_row.map(&:size) # => [3, 3]
23
+ # tile.each_col.map(&:size) # => [2, 2, 2]
24
+ # @see Sevgi::Graphics::Mixtures::Tile
6
25
  class Tile
7
26
  include Enumerable
8
27
 
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Sundries
5
5
  # Component version.
6
- VERSION = "0.95.0"
6
+ VERSION = "0.98.2"
7
7
  end
8
8
  end
@@ -15,9 +15,18 @@ require_relative "sundries/version"
15
15
  module Sevgi
16
16
  # Layout, tiling, grid, and export helpers shared by Sevgi consumers.
17
17
  #
18
- # This component loads its eager public surfaces directly. `Ruler`, `Tile`, and `Grid` therefore require
19
- # `sevgi-function`, `sevgi-geometry`, and `sevgi-graphics` as runtime dependencies of the `sevgi-sundries` gem.
20
- # Native PDF/PNG export gems are optional and loaded only by {Export} when export is used.
18
+ # {Ruler} fits repeatable distances into a span; {Grid} combines two rulers
19
+ # and exposes drawable lines; {Tile} repeats geometry by rows and columns.
20
+ # These layout values can be computed without an SVG document and then passed
21
+ # to Graphics. Native PDF/PNG dependencies remain lazy and are loaded only by
22
+ # {Export}.
23
+ # @example Load the component and build a tiled geometry layout
24
+ # require "sevgi/sundries"
25
+ #
26
+ # cell = Sevgi::Geometry::Rect[8, 4]
27
+ # Sevgi::Sundries::Tile.new(cell, nx: 3, ny: 2).box.deconstruct
28
+ # @see Sevgi::Graphics::Canvas
29
+ # @see https://sevgi.roktas.dev/sundries/ Sundries guide
21
30
  module Sundries
22
31
  end
23
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-sundries
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.95.0
4
+ version: 0.98.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,43 +15,43 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.95.0
18
+ version: 0.98.2
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.95.0
25
+ version: 0.98.2
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sevgi-geometry
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.95.0
32
+ version: 0.98.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.95.0
39
+ version: 0.98.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: sevgi-graphics
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 0.95.0
46
+ version: 0.98.2
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 0.95.0
54
- description: Enhances the Sevgi toolkit with helper objects.
53
+ version: 0.98.2
54
+ description: Includes grid, ruler, tile, and optional PDF or PNG export support.
55
55
  email: roktas@gmail.com
56
56
  executables: []
57
57
  extensions: []
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  - !ruby/object:Gem::Version
91
91
  version: '0'
92
92
  requirements: []
93
- rubygems_version: 4.0.11
93
+ rubygems_version: 4.0.16
94
94
  specification_version: 4
95
- summary: Various instrument objects for the Sevgi toolkit.
95
+ summary: Layout helpers and export tools for Sevgi.
96
96
  test_files: []