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 +4 -4
- data/CHANGELOG.md +125 -0
- data/README.md +6 -6
- data/lib/sevgi/sundries/export/native.rb +21 -15
- data/lib/sevgi/sundries/export/system.rb +6 -6
- data/lib/sevgi/sundries/export.rb +37 -26
- data/lib/sevgi/sundries/grid.rb +97 -25
- data/lib/sevgi/sundries/ruler.rb +88 -28
- data/lib/sevgi/sundries/tile.rb +19 -0
- data/lib/sevgi/sundries/version.rb +1 -1
- data/lib/sevgi/sundries.rb +12 -3
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7df3ccce8a77f50c584bd9ced3fec7fa19c3efb7c458d8dc248ca3629666fca3
|
|
4
|
+
data.tar.gz: 690408c4e42c34eb5fe500d405a6f2532c9b7f1b6350784d176ba77f40135ea8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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 [
|
|
33
|
-
# @raise [Sevgi::ArgumentError] when output,
|
|
34
|
-
#
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
312
|
-
ArgumentError.("Export output must be a String-like
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
#
|
|
16
|
-
|
|
25
|
+
# File extensions mapped to export format names.
|
|
26
|
+
# @api private
|
|
27
|
+
EXTENSIONS = {
|
|
17
28
|
".pdf" => :pdf,
|
|
18
29
|
".png" => :png
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
#
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
data/lib/sevgi/sundries/grid.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
@
|
|
33
|
-
@
|
|
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
|
|
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:
|
|
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
|
|
104
|
+
# Axis wrapper exposing grid line queries for one line direction.
|
|
56
105
|
class Axis < DelegateClass(Ruler)
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|
|
77
|
-
@halve = Halve.new
|
|
78
|
-
@minor = Minor.new
|
|
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 ||=
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
def
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
def
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
def
|
|
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
|
data/lib/sevgi/sundries/ruler.rb
CHANGED
|
@@ -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,
|
|
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
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
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.
|
|
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
|
|
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
|
|
185
|
-
# @raise [Sevgi::ArgumentError] when margin is not
|
|
186
|
-
# @raise [Sevgi::ArgumentError] when margin is
|
|
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:,
|
|
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:,
|
|
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,
|
|
234
|
+
def expand = self.class.new(unit: sub.u, multiple: 1, brut: d + waste, margins:)
|
|
205
235
|
|
|
206
|
-
# Returns
|
|
207
|
-
# @return [Float]
|
|
208
|
-
def
|
|
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
|
|
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:,
|
|
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
|
|
data/lib/sevgi/sundries/tile.rb
CHANGED
|
@@ -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
|
|
data/lib/sevgi/sundries.rb
CHANGED
|
@@ -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
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
54
|
-
description:
|
|
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.
|
|
93
|
+
rubygems_version: 4.0.16
|
|
94
94
|
specification_version: 4
|
|
95
|
-
summary:
|
|
95
|
+
summary: Layout helpers and export tools for Sevgi.
|
|
96
96
|
test_files: []
|