sevgi-graphics 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 +8 -8
- data/lib/sevgi/graphics/attribute.rb +160 -37
- data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
- data/lib/sevgi/graphics/auxiliary/content.rb +54 -34
- data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
- data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
- data/lib/sevgi/graphics/auxiliary.rb +1 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +236 -102
- data/lib/sevgi/graphics/element.rb +114 -31
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +59 -20
- data/lib/sevgi/graphics/mixtures/duplicate.rb +49 -15
- data/lib/sevgi/graphics/mixtures/export.rb +52 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +26 -8
- data/lib/sevgi/graphics/mixtures/inkscape.rb +201 -47
- data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
- data/lib/sevgi/graphics/mixtures/render.rb +52 -28
- data/lib/sevgi/graphics/mixtures/save.rb +79 -35
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
- data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +14 -5
- data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +62 -23
- data/lib/sevgi/graphics/mixtures.rb +15 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +58 -12
- metadata +7 -6
|
@@ -8,52 +8,96 @@ module Sevgi
|
|
|
8
8
|
# DSL helpers for writing rendered SVG output.
|
|
9
9
|
module Save
|
|
10
10
|
# Default SVG extension.
|
|
11
|
+
# @api private
|
|
11
12
|
EXT = ".svg"
|
|
12
13
|
|
|
14
|
+
private_constant :EXT
|
|
15
|
+
|
|
16
|
+
# Change-aware file writer with optional backup support.
|
|
17
|
+
# @api private
|
|
18
|
+
class Writer
|
|
19
|
+
# Writes content when it differs from the destination.
|
|
20
|
+
# @param path [String] expanded output path
|
|
21
|
+
# @param content [String] rendered content
|
|
22
|
+
# @param backup_suffix [String, nil] suffix used for an existing-file backup
|
|
23
|
+
# @yield [content] optionally normalizes old and new content for change detection
|
|
24
|
+
# @yieldparam content [String] old or new content
|
|
25
|
+
# @yieldreturn [String] normalized content
|
|
26
|
+
# @return [String, nil] expanded path when written, otherwise nil
|
|
27
|
+
# @raise [SystemCallError] when the destination or backup cannot be created, read, or written
|
|
28
|
+
def self.call(path, content, backup_suffix: nil, &filter)
|
|
29
|
+
output = "#{content.chomp}\n"
|
|
30
|
+
|
|
31
|
+
return unless F.changed?(path, output, &filter)
|
|
32
|
+
|
|
33
|
+
::FileUtils.mkdir_p(::File.dirname(path))
|
|
34
|
+
if backup_suffix && !backup_suffix.empty? && ::File.exist?(path)
|
|
35
|
+
::FileUtils.cp(path, "#{path}#{backup_suffix}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
path.tap { ::File.write(path, output) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_constant :Writer
|
|
43
|
+
|
|
13
44
|
# Writes rendered SVG to standard output.
|
|
14
|
-
# @param kwargs [Hash] render options
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
# @param kwargs [Hash] pre-render and renderer options accepted by {Sevgi::Graphics::Document::Proto#call}
|
|
46
|
+
# @return [nil]
|
|
47
|
+
# @raise [Sevgi::ArgumentError] when a render option or XML-bound value is invalid
|
|
48
|
+
# @raise [Sevgi::ValidationError] when validation is enabled and the document violates the SVG standard
|
|
49
|
+
# @raise [Sevgi::Graphics::LintError] when linting is enabled and the document has structural conflicts
|
|
50
|
+
# @see Sevgi::Graphics::Document::Proto#call
|
|
51
|
+
def Out(**kwargs)
|
|
52
|
+
F.out(self.(**kwargs))
|
|
21
53
|
end
|
|
22
54
|
|
|
23
|
-
# Saves rendered SVG
|
|
24
|
-
#
|
|
25
|
-
#
|
|
55
|
+
# Saves rendered SVG when its content differs from the destination.
|
|
56
|
+
# Relative destinations are expanded before being returned. When a non-empty backup suffix is given, an
|
|
57
|
+
# existing destination is copied immediately before replacement; unchanged saves leave both files untouched.
|
|
58
|
+
# Missing parent directories are created. An existing directory target uses the default file name.
|
|
59
|
+
# @example Save to a relative destination
|
|
60
|
+
# path = Sevgi::Graphics.SVG(:minimal).Save("build/drawing.svg")
|
|
61
|
+
# path == File.expand_path("build/drawing.svg") # => true
|
|
62
|
+
# @param path [String, #to_path, nil] output path or existing directory
|
|
63
|
+
# @param default [String, #to_path, nil] default output path
|
|
26
64
|
# @param backup_suffix [String, nil] suffix used for an existing-file backup
|
|
27
|
-
# @
|
|
28
|
-
# @
|
|
29
|
-
# @
|
|
30
|
-
# @
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
::FileUtils.mkdir_p(::File.dirname(path))
|
|
41
|
-
if backup_suffix && !backup_suffix.empty? && ::File.exist?(path)
|
|
42
|
-
::FileUtils.cp(path, "#{path}#{backup_suffix}")
|
|
43
|
-
end
|
|
65
|
+
# @param kwargs [Hash] pre-render and renderer options accepted by {Sevgi::Graphics::Document::Proto#call}
|
|
66
|
+
# @yield [content] optionally normalizes old and new content for change detection
|
|
67
|
+
# @yieldparam content [String] old or new SVG source
|
|
68
|
+
# @yieldreturn [String] normalized SVG source
|
|
69
|
+
# @return [String, nil] expanded path when written, or nil when unchanged
|
|
70
|
+
# @raise [Sevgi::ArgumentError] when a selected path/default, render option, or XML-bound value is invalid
|
|
71
|
+
# @raise [Sevgi::ValidationError] when validation is enabled and the document violates the SVG standard
|
|
72
|
+
# @raise [Sevgi::Graphics::LintError] when linting is enabled and the document has structural conflicts
|
|
73
|
+
# @raise [SystemCallError] when the destination or backup cannot be created, read, or written
|
|
74
|
+
# @see Sevgi::Graphics::Document::Proto#call
|
|
75
|
+
def Save(path = nil, default: nil, backup_suffix: nil, **kwargs, &filter)
|
|
76
|
+
default = F.subext(EXT, caller_locations(1..1).first.path) if default.nil?
|
|
77
|
+
path = Path.resolve(path, default:, context: "Save")
|
|
44
78
|
|
|
45
|
-
|
|
79
|
+
Writer.(path, self.(**kwargs), backup_suffix:, &filter)
|
|
46
80
|
end
|
|
47
81
|
|
|
48
82
|
# Writes rendered SVG to a path.
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @
|
|
52
|
-
# @
|
|
53
|
-
# @
|
|
54
|
-
# @
|
|
83
|
+
# Missing parent directories are created. Unlike {#Save}, a directory is not treated as a request for a default
|
|
84
|
+
# file name.
|
|
85
|
+
# @param path [String, #to_path] output file path
|
|
86
|
+
# @param kwargs [Hash] pre-render and renderer options accepted by {Sevgi::Graphics::Document::Proto#call}
|
|
87
|
+
# @yield [content] optionally normalizes old and new content for change detection
|
|
88
|
+
# @yieldparam content [String] old or new SVG source
|
|
89
|
+
# @yieldreturn [String] normalized SVG source
|
|
90
|
+
# @return [String, nil] expanded path when written, or nil when unchanged
|
|
91
|
+
# @raise [Sevgi::ArgumentError] when path, a render option, or an XML-bound value is invalid
|
|
92
|
+
# @raise [Sevgi::ValidationError] when validation is enabled and the document violates the SVG standard
|
|
93
|
+
# @raise [Sevgi::Graphics::LintError] when linting is enabled and the document has structural conflicts
|
|
94
|
+
# @raise [SystemCallError] when the destination cannot be read or written
|
|
95
|
+
# @see Sevgi::Graphics::Document::Proto#call
|
|
55
96
|
def Write(path, **kwargs, &filter)
|
|
56
|
-
|
|
97
|
+
path = Path.(path, context: "Write path")
|
|
98
|
+
ArgumentError.("Write path must name a file") if ::File.directory?(path)
|
|
99
|
+
|
|
100
|
+
Writer.(path, self.(**kwargs), &filter)
|
|
57
101
|
end
|
|
58
102
|
end
|
|
59
103
|
end
|
|
@@ -5,20 +5,89 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for expanding callable modules into SVG symbols.
|
|
7
7
|
module Symbols
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
8
|
+
# Builds one symbol set without adding helper methods to the document DSL.
|
|
9
|
+
# @api private
|
|
10
|
+
class Expansion
|
|
11
|
+
# Creates a symbol expansion.
|
|
12
|
+
# @param receiver [Sevgi::Graphics::Element] parent element
|
|
13
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
14
|
+
# @return [void]
|
|
15
|
+
def initialize(receiver, mod)
|
|
16
|
+
@receiver = receiver
|
|
17
|
+
@mod = mod
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Builds a defs container and populates it from the callable module.
|
|
21
|
+
# @param args [Array<Object>] callable positional arguments
|
|
22
|
+
# @param attributes [Hash] defs attributes
|
|
23
|
+
# @param ids [#call, nil] symbol id mapper
|
|
24
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
25
|
+
# @param block [Proc, nil] callable block argument
|
|
26
|
+
# @return [Sevgi::Graphics::Element] defs element
|
|
27
|
+
# @raise [Sevgi::ArgumentError] when an input channel is invalid
|
|
28
|
+
def call(*args, attributes:, ids:, **kwargs, &block)
|
|
29
|
+
methods = Graphics::Module.__send__(:callables, @mod)
|
|
30
|
+
ArgumentError.("Defs attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
31
|
+
ArgumentError.("Symbol ids must respond to call") if ids && !ids.respond_to?(:call)
|
|
32
|
+
|
|
33
|
+
defaults = @mod.name ? {id: F.demodulize(@mod.name).to_sym} : {}
|
|
34
|
+
attributes = Attribute.defaults(attributes, **defaults)
|
|
35
|
+
@args, @kwargs, @block = args, kwargs, block
|
|
36
|
+
@receiver.defs(**attributes).tap { populate(it, methods, ids) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Adds bases and symbols to a defs element.
|
|
42
|
+
# @param defs [Sevgi::Graphics::Element] defs element
|
|
43
|
+
# @param methods [Array<UnboundMethod>] callable methods
|
|
44
|
+
# @param ids [#call, nil] symbol id mapper
|
|
45
|
+
# @return [void]
|
|
46
|
+
def populate(defs, methods, ids)
|
|
47
|
+
context = Graphics::Module.__send__(:context, @mod, defs)
|
|
48
|
+
Graphics::Module.__send__(:bases, @mod).each { context.instance_exec(&it) }
|
|
49
|
+
methods.each { draw(defs, it, ids) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Adds one callable symbol.
|
|
53
|
+
# @param defs [Sevgi::Graphics::Element] defs element
|
|
54
|
+
# @param method [UnboundMethod] callable method
|
|
55
|
+
# @param ids [#call, nil] symbol id mapper
|
|
56
|
+
# @return [Object, nil] callable return value
|
|
57
|
+
def draw(defs, method, ids)
|
|
58
|
+
name = method.name
|
|
59
|
+
symbol = defs.symbol(id: ids ? ids.call(name) : name.to_s.tr("_", "-"))
|
|
60
|
+
symbol.title(name.to_s.split("_").map(&:capitalize).join(" "))
|
|
61
|
+
context = Graphics::Module.__send__(:context, @mod, symbol)
|
|
62
|
+
Graphics::Module.__send__(:invoke, context, symbol, [method], *@args, **@kwargs, &@block)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_constant :Expansion
|
|
67
|
+
|
|
68
|
+
# Renders module callables as symbols under defs. Named modules default the defs id to their final constant name;
|
|
69
|
+
# anonymous modules omit the id unless supplied.
|
|
70
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
10
71
|
# @param args [Array<Object>] callable arguments
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# @
|
|
14
|
-
#
|
|
72
|
+
# Base blocks run once in the defs element before symbols are created. Positional arguments, keyword arguments,
|
|
73
|
+
# and the block are forwarded to each callable.
|
|
74
|
+
# @example Expand named drawing methods into reusable symbols
|
|
75
|
+
# icons = Module.new do
|
|
76
|
+
# extend Sevgi::Graphics::Module
|
|
77
|
+
# def dot = circle r: 2
|
|
78
|
+
# def tick = path d: "M 0 2 L 2 4 L 6 0"
|
|
79
|
+
# end
|
|
80
|
+
# Sevgi::Graphics.SVG(:minimal) { Symbols icons }
|
|
81
|
+
# @param attributes [Hash] defs attributes; String and Symbol names are normalized and must not collide
|
|
82
|
+
# @param ids [#call, nil] optional callable mapping each method name to a symbol id
|
|
83
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
84
|
+
# @yield forwarded to each callable
|
|
85
|
+
# @yieldreturn [Object] callable-defined block result
|
|
15
86
|
# @return [Sevgi::Graphics::Element] defs element
|
|
16
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
title(name.to_s.split("_").map(&:capitalize).join(" "))
|
|
21
|
-
end
|
|
87
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module, attributes is not a Hash, or ids is
|
|
88
|
+
# not callable
|
|
89
|
+
def Symbols(mod, *args, attributes: {}, ids: nil, **kwargs, &block)
|
|
90
|
+
Expansion.new(self, mod).call(*args, attributes:, ids:, **kwargs, &block)
|
|
22
91
|
end
|
|
23
92
|
end
|
|
24
93
|
end
|
|
@@ -3,22 +3,35 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# DSL helpers for defining one SVG template and repeating it through `use` elements.
|
|
7
|
+
#
|
|
8
|
+
# Use {Sevgi::Sundries::Tile} instead when Ruby code needs inspectable repeated geometry or row/column bounds
|
|
9
|
+
# rather than SVG references.
|
|
10
|
+
# @see Sevgi::Sundries::Tile
|
|
11
|
+
# @see https://sevgi.roktas.dev/sundries/#choose-a-layout-model Choosing a layout model
|
|
8
12
|
module Tile
|
|
9
|
-
#
|
|
13
|
+
# Stable prefix used for generated tile CSS classes.
|
|
10
14
|
PREFIX = "tile"
|
|
11
15
|
|
|
12
16
|
# Builds a two-dimensional tile grid.
|
|
17
|
+
# Each use id has the form `id-row-column`, with one-based row and column numbers. Generated classes identify
|
|
18
|
+
# the one-based row and column and mark their first and last positions. A block defines the referenced template
|
|
19
|
+
# as a group under `defs` before the uses are added.
|
|
20
|
+
# @example Define and customize a tile grid
|
|
21
|
+
# customize = proc { |use, x:, y:, nx:, ny:| use[:opacity] = (x + y + 1).fdiv(nx + ny) }
|
|
22
|
+
# Sevgi::Graphics.SVG(:minimal) do
|
|
23
|
+
# Tile("dot", nx: 2, dx: 10, ny: 2, dy: 10, proc: customize) { circle r: 2 }
|
|
24
|
+
# end
|
|
13
25
|
# @param id [String] referenced template id
|
|
14
26
|
# @param nx [Integer] number of columns
|
|
15
|
-
# @param dx [Numeric] horizontal spacing
|
|
16
|
-
# @param ox [Numeric] horizontal offset
|
|
27
|
+
# @param dx [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
28
|
+
# @param ox [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
17
29
|
# @param ny [Integer] number of rows
|
|
18
|
-
# @param dy [Numeric] vertical spacing
|
|
19
|
-
# @param oy [Numeric] vertical offset
|
|
20
|
-
# @param proc [Proc, nil] optional
|
|
21
|
-
#
|
|
30
|
+
# @param dy [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
31
|
+
# @param oy [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
32
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, y:, nx:, ny:)`, with
|
|
33
|
+
# zero-based coordinates and total counts; the callback may mutate the element and its return value is ignored
|
|
34
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
22
35
|
# @yieldreturn [Object] ignored block result
|
|
23
36
|
# @return [Sevgi::Graphics::Element] self
|
|
24
37
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
@@ -33,14 +46,9 @@ module Sevgi
|
|
|
33
46
|
proc: nil,
|
|
34
47
|
&block
|
|
35
48
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# rubocop:disable Style/NestedTernaryOperator
|
|
40
|
-
# for pretty kwargs handling
|
|
41
|
-
x.zero? ? (y.zero? ? {} : {y:}) : (y.zero? ? {x:} : {x:, y:})
|
|
42
|
-
# rubocop:enable Style/NestedTernaryOperator
|
|
43
|
-
end
|
|
49
|
+
id, nx, dx, ox, ny, dy, oy, callback = Helper
|
|
50
|
+
.normalize(id:, nx:, dx:, ox:, ny:, dy:, oy:, proc:)
|
|
51
|
+
.values_at(:id, :nx, :dx, :ox, :ny, :dy, :oy, :proc)
|
|
44
52
|
|
|
45
53
|
defs { g(id:, &block) } if block
|
|
46
54
|
|
|
@@ -52,34 +60,36 @@ module Sevgi
|
|
|
52
60
|
cs = Helper.classify(as: "col", index: x, upper: nx)
|
|
53
61
|
|
|
54
62
|
element = use(
|
|
55
|
-
id: [
|
|
56
|
-
href: "##{
|
|
63
|
+
id: [id, y + 1, x + 1].join("-"),
|
|
64
|
+
href: "##{id}",
|
|
57
65
|
class: [*rs, *cs].join(" "),
|
|
58
|
-
**
|
|
66
|
+
**Helper.coordinates(
|
|
67
|
+
x: Scalar.number((x * dx) + ox, context: "tile", field: :x),
|
|
68
|
+
y: Scalar.number((y * dy) + oy, context: "tile", field: :y)
|
|
69
|
+
)
|
|
59
70
|
)
|
|
60
|
-
|
|
71
|
+
callback&.call(element, x:, y:, nx:, ny:)
|
|
61
72
|
end
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
# Builds a one-dimensional horizontal tile row.
|
|
78
|
+
# Each use id has the form `id-column`, with a one-based column number. Generated classes identify the column and
|
|
79
|
+
# mark its first and last positions. A block defines the referenced template as a group under `defs` before the
|
|
80
|
+
# uses are added.
|
|
67
81
|
# @param id [String] referenced template id
|
|
68
82
|
# @param n [Integer] number of instances
|
|
69
|
-
# @param d [Numeric] horizontal spacing
|
|
70
|
-
# @param o [Numeric] horizontal offset
|
|
71
|
-
# @param proc [Proc, nil] optional
|
|
72
|
-
#
|
|
83
|
+
# @param d [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
84
|
+
# @param o [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
85
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, n:)`, with a zero-based column
|
|
86
|
+
# and total count; the callback may mutate the element and its return value is ignored
|
|
87
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
73
88
|
# @yieldreturn [Object] ignored block result
|
|
74
89
|
# @return [Sevgi::Graphics::Element] self
|
|
75
90
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
76
91
|
def TileX(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
77
|
-
Helper.
|
|
78
|
-
|
|
79
|
-
href, coords = id, proc do |x|
|
|
80
|
-
# for pretty kwargs handling
|
|
81
|
-
x.zero? ? {} : {x:}
|
|
82
|
-
end
|
|
92
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
83
93
|
|
|
84
94
|
defs { g(id:, &block) } if block
|
|
85
95
|
|
|
@@ -88,33 +98,32 @@ module Sevgi
|
|
|
88
98
|
cs = Helper.classify(as: "col", index: x, upper: n)
|
|
89
99
|
|
|
90
100
|
element = use(
|
|
91
|
-
id: [
|
|
92
|
-
href: "##{
|
|
101
|
+
id: [id, x + 1].join("-"),
|
|
102
|
+
href: "##{id}",
|
|
93
103
|
class: cs.join(" "),
|
|
94
|
-
**
|
|
104
|
+
**Helper.coordinates(x: Scalar.number((x * d) + o, context: "tile", field: :x))
|
|
95
105
|
)
|
|
96
|
-
|
|
106
|
+
callback&.call(element, x:, n:)
|
|
97
107
|
end
|
|
98
108
|
end
|
|
99
109
|
end
|
|
100
110
|
|
|
101
111
|
# Builds a one-dimensional vertical tile column.
|
|
112
|
+
# Each use id has the form `id-row`, with a one-based row number. Generated classes identify the row and mark its
|
|
113
|
+
# first and last positions. A block defines the referenced template as a group under `defs` before the uses are
|
|
114
|
+
# added.
|
|
102
115
|
# @param id [String] referenced template id
|
|
103
116
|
# @param n [Integer] number of instances
|
|
104
|
-
# @param d [Numeric] vertical spacing
|
|
105
|
-
# @param o [Numeric] vertical offset
|
|
106
|
-
# @param proc [Proc, nil] optional
|
|
107
|
-
#
|
|
117
|
+
# @param d [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
118
|
+
# @param o [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
119
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, y:, n:)`, with a zero-based row and
|
|
120
|
+
# total count; the callback may mutate the element and its return value is ignored
|
|
121
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
108
122
|
# @yieldreturn [Object] ignored block result
|
|
109
123
|
# @return [Sevgi::Graphics::Element] self
|
|
110
124
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
111
125
|
def TileY(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
112
|
-
Helper.
|
|
113
|
-
|
|
114
|
-
href, coords = id, proc do |y|
|
|
115
|
-
# for pretty kwargs handling
|
|
116
|
-
y.zero? ? {} : {y:}
|
|
117
|
-
end
|
|
126
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
118
127
|
|
|
119
128
|
defs { g(id:, &block) } if block
|
|
120
129
|
|
|
@@ -123,12 +132,12 @@ module Sevgi
|
|
|
123
132
|
rs = Helper.classify(as: "row", index: y, upper: n)
|
|
124
133
|
|
|
125
134
|
element = use(
|
|
126
|
-
id: [
|
|
127
|
-
href: "##{
|
|
135
|
+
id: [id, y + 1].join("-"),
|
|
136
|
+
href: "##{id}",
|
|
128
137
|
class: rs.join(" "),
|
|
129
|
-
**
|
|
138
|
+
**Helper.coordinates(y: Scalar.number((y * d) + o, context: "tile", field: :y))
|
|
130
139
|
)
|
|
131
|
-
|
|
140
|
+
callback&.call(element, y:, n:)
|
|
132
141
|
end
|
|
133
142
|
end
|
|
134
143
|
end
|
|
@@ -138,18 +147,14 @@ module Sevgi
|
|
|
138
147
|
module Helper
|
|
139
148
|
extend self
|
|
140
149
|
|
|
150
|
+
FINITE = %i[d dx dy o ox oy].freeze
|
|
151
|
+
|
|
141
152
|
# Argument validators for tile helpers.
|
|
142
153
|
ASSERTION = {
|
|
143
154
|
id: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
|
|
144
155
|
n: proc { |name, value| positive_integer_issue(name, value) },
|
|
145
156
|
nx: proc { |name, value| positive_integer_issue(name, value) },
|
|
146
157
|
ny: proc { |name, value| positive_integer_issue(name, value) },
|
|
147
|
-
d: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
148
|
-
dx: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
149
|
-
dy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
150
|
-
o: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
151
|
-
ox: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
152
|
-
oy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
153
158
|
proc: proc { |name, value| "Argument '#{name}' must be a proc" unless value.nil? || value.is_a?(::Proc) }
|
|
154
159
|
}.freeze
|
|
155
160
|
|
|
@@ -161,22 +166,33 @@ module Sevgi
|
|
|
161
166
|
"Argument '#{name}' must be a positive integer" unless value.is_a?(::Integer) && value.positive?
|
|
162
167
|
end
|
|
163
168
|
|
|
164
|
-
# Validates tile arguments.
|
|
169
|
+
# Validates and normalizes tile arguments.
|
|
165
170
|
# @param kwargs [Hash] tile arguments
|
|
166
|
-
# @return [
|
|
171
|
+
# @return [Hash] independent arguments with spacing and offsets normalized to SVG numbers
|
|
167
172
|
# @raise [Sevgi::ArgumentError] when an argument is missing or invalid
|
|
168
|
-
def
|
|
169
|
-
kwargs.
|
|
170
|
-
|
|
173
|
+
def normalize(**kwargs)
|
|
174
|
+
kwargs.to_h do |name, value|
|
|
175
|
+
ArgumentError.("Argument '#{name}' required") if value == Undefined
|
|
176
|
+
[name, normalize_value(name, value)]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
171
179
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
180
|
+
def coordinates(**coordinates)
|
|
181
|
+
coordinates.reject { |_, value| value.zero? }
|
|
182
|
+
end
|
|
176
183
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
def normalize_value(name, value)
|
|
185
|
+
return number(name, value) if FINITE.include?(name)
|
|
186
|
+
return value unless (assertion = ASSERTION[name])
|
|
187
|
+
return value unless (issue = assertion.call(name, value))
|
|
188
|
+
|
|
189
|
+
ArgumentError.(issue)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def number(name, value)
|
|
193
|
+
Scalar.number(value, context: "tile", field: name)
|
|
194
|
+
rescue ::Sevgi::ArgumentError
|
|
195
|
+
ArgumentError.("Argument '#{name}' must be a finite real number")
|
|
180
196
|
end
|
|
181
197
|
|
|
182
198
|
# Returns positional tile CSS classes.
|
|
@@ -191,6 +207,8 @@ module Sevgi
|
|
|
191
207
|
classes << "#{PREFIX}-#{as}-last" if index + 1 == upper
|
|
192
208
|
end
|
|
193
209
|
end
|
|
210
|
+
|
|
211
|
+
private :normalize_value, :number
|
|
194
212
|
end
|
|
195
213
|
|
|
196
214
|
private_constant :Helper
|