sevgi-graphics 0.94.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 +275 -2
- data/LICENSE +672 -3
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +271 -54
- data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
- data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
- data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
- data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +3 -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 +370 -124
- data/lib/sevgi/graphics/element.rb +143 -33
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +143 -33
- data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
- data/lib/sevgi/graphics/mixtures/export.rb +58 -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 +40 -5
- data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
- data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
- data/lib/sevgi/graphics/mixtures/render.rb +88 -19
- data/lib/sevgi/graphics/mixtures/save.rb +79 -26
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
- data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
- data/lib/sevgi/graphics/mixtures.rb +19 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +75 -28
- metadata +10 -6
|
@@ -5,15 +5,17 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for collecting and hiding SVG ids.
|
|
7
7
|
module Identify
|
|
8
|
-
#
|
|
8
|
+
# Immutable snapshot of every rendered element id under a subtree. Keys are the serialized id values, including
|
|
9
|
+
# `"false"` and the empty String. Keys and containers are owned by the index; values retain references to the
|
|
10
|
+
# elements present when the snapshot is built. Later tree changes require a new index.
|
|
9
11
|
class Identifiers
|
|
10
12
|
# @return [Sevgi::Graphics::Element] indexed root element
|
|
11
13
|
attr_reader :element
|
|
12
14
|
|
|
13
|
-
# @return [Hash<String, Sevgi::Graphics::Element>]
|
|
15
|
+
# @return [Hash<String, Sevgi::Graphics::Element>] frozen namespace keyed by serialized rendered ids
|
|
14
16
|
attr_reader :namespace
|
|
15
17
|
|
|
16
|
-
# @return [Hash<String, Array<Sevgi::Graphics::Element>>] duplicate
|
|
18
|
+
# @return [Hash<String, Array<Sevgi::Graphics::Element>>] frozen duplicate groups keyed by serialized ids
|
|
17
19
|
attr_reader :collision
|
|
18
20
|
|
|
19
21
|
# Builds an id index for an element subtree.
|
|
@@ -25,6 +27,10 @@ module Sevgi
|
|
|
25
27
|
@collision = {}
|
|
26
28
|
|
|
27
29
|
build
|
|
30
|
+
@namespace.freeze
|
|
31
|
+
@collision.each_value(&:freeze)
|
|
32
|
+
@collision.freeze
|
|
33
|
+
freeze
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
# Reports whether duplicate ids were found.
|
|
@@ -33,21 +39,20 @@ module Sevgi
|
|
|
33
39
|
!@collision.empty?
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
def [](
|
|
41
|
-
@namespace[*]
|
|
42
|
-
end
|
|
42
|
+
# Returns the first element registered for an id in the snapshot.
|
|
43
|
+
# Duplicate entries are available through {#collision}.
|
|
44
|
+
# @param id [String] serialized rendered SVG id
|
|
45
|
+
# @return [Sevgi::Graphics::Element, nil]
|
|
46
|
+
def [](id) = @namespace[id]
|
|
43
47
|
|
|
44
48
|
private
|
|
45
49
|
|
|
46
50
|
def build
|
|
47
51
|
element.Traverse() do |element|
|
|
48
|
-
next unless (
|
|
52
|
+
next unless element.attributes.has?(:id)
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
value = element[:id]
|
|
55
|
+
id = Attribute.xml_text(value).dup.freeze
|
|
51
56
|
|
|
52
57
|
if @namespace.key?(id)
|
|
53
58
|
(@collision[id] ||= [@namespace[id]]) << element
|
|
@@ -58,18 +63,26 @@ module Sevgi
|
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
|
|
61
|
-
# Moves visible id attributes to
|
|
66
|
+
# Moves visible id attributes to non-rendering `-id` metadata throughout the subtree.
|
|
67
|
+
# A pre-existing `-id` takes precedence over the visible id.
|
|
62
68
|
# @return [Sevgi::Graphics::Element] self
|
|
69
|
+
# @example Hide ids while retaining their source identity
|
|
70
|
+
# document = Sevgi::Graphics.SVG { rect id: "source-id" }
|
|
71
|
+
# document.Disidentify
|
|
72
|
+
# document.children.first[:"-id"] # => "source-id"
|
|
63
73
|
def Disidentify
|
|
64
74
|
Traverse do |element|
|
|
65
|
-
next unless element
|
|
75
|
+
next unless element.attributes.has?(:id)
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
id = element.attributes.delete(:id)
|
|
78
|
+
metadata = :"#{Attributes::META_PREFIX}id"
|
|
79
|
+
element[metadata] = id unless element.attributes.has?(metadata)
|
|
68
80
|
end
|
|
69
81
|
end
|
|
70
82
|
|
|
71
|
-
# Builds an id index for
|
|
72
|
-
#
|
|
83
|
+
# Builds an immutable id index snapshot for the current element subtree.
|
|
84
|
+
# Rebuild the index to observe later id or tree changes.
|
|
85
|
+
# @return [Sevgi::Graphics::Mixtures::Identify::Identifiers] new snapshot retaining indexed element references
|
|
73
86
|
def Identifiers = Identifiers.new(self)
|
|
74
87
|
end
|
|
75
88
|
end
|
|
@@ -4,6 +4,33 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for including derendered SVG/XML fragments.
|
|
7
|
+
#
|
|
8
|
+
# @!method Include(file, id, omit: nil)
|
|
9
|
+
# Includes a derendered node matching an id.
|
|
10
|
+
# SVG/XML content is treated as data and is not evaluated as Ruby source.
|
|
11
|
+
# @example Import a fragment without editor ids and inline styles
|
|
12
|
+
# Sevgi::Graphics.SVG do
|
|
13
|
+
# Include "badge.svg", "mark", omit: %i[id style]
|
|
14
|
+
# end
|
|
15
|
+
# @param file [String] source SVG/XML file
|
|
16
|
+
# @param id [String, Symbol] source node id
|
|
17
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
18
|
+
# subtree after id selection
|
|
19
|
+
# @return [Sevgi::Graphics::Element, nil] included element, or nil when it produces no graphics output
|
|
20
|
+
# @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
|
|
21
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
22
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
23
|
+
# @!method IncludeChildren(file, id, omit: nil)
|
|
24
|
+
# Includes the children of a derendered node matching an id.
|
|
25
|
+
# SVG/XML content is treated as data and is not evaluated as Ruby source.
|
|
26
|
+
# @param file [String] source SVG/XML file
|
|
27
|
+
# @param id [String, Symbol] source node id
|
|
28
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
29
|
+
# subtree after id selection
|
|
30
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
31
|
+
# @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
|
|
32
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
33
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
7
34
|
module Include
|
|
8
35
|
require "sevgi/derender"
|
|
9
36
|
|
|
@@ -12,38 +39,46 @@ module Sevgi
|
|
|
12
39
|
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
13
40
|
# @param file [String] source SVG/XML file
|
|
14
41
|
# @param id [String, Symbol] source node id
|
|
42
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
43
|
+
# subtree after id selection
|
|
15
44
|
# @return [Sevgi::Graphics::Element, nil] included element, or nil when the selected node produces no graphics
|
|
16
45
|
# output
|
|
17
46
|
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
18
47
|
# absent
|
|
48
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
19
49
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
20
|
-
def Include(file, id) = Derender.evaluate_file(file, self, id:)
|
|
50
|
+
def Include(file, id, omit: nil) = Derender.evaluate_file(file, self, id:, omit:)
|
|
21
51
|
|
|
22
52
|
# Includes the children of a derendered node matching an id.
|
|
23
53
|
#
|
|
24
54
|
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
25
55
|
# @param file [String] source SVG/XML file
|
|
26
56
|
# @param id [String, Symbol] source node id
|
|
27
|
-
# @
|
|
57
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
58
|
+
# subtree after id selection
|
|
59
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
28
60
|
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
29
61
|
# absent
|
|
62
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
30
63
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
31
|
-
def IncludeChildren(file, id) = Derender.
|
|
64
|
+
def IncludeChildren(file, id, omit: nil) = Derender.evaluate_children_file(file, self, id:, omit:)
|
|
32
65
|
rescue ::LoadError => e
|
|
33
66
|
raise unless e.path == "sevgi/derender"
|
|
34
67
|
|
|
35
|
-
# @overload IncludeChildren(file, id)
|
|
68
|
+
# @overload IncludeChildren(file, id, omit: nil)
|
|
36
69
|
# Raises because sevgi/derender is unavailable.
|
|
37
70
|
# @param file [String] source SVG/XML file
|
|
38
71
|
# @param id [String, Symbol] source node id
|
|
72
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
|
|
39
73
|
# @return [void]
|
|
40
74
|
# @raise [Sevgi::MissingComponentError] always
|
|
41
75
|
def IncludeChildren(...) = MissingComponentError.("sevgi/derender")
|
|
42
76
|
|
|
43
|
-
# @overload Include(file, id)
|
|
77
|
+
# @overload Include(file, id, omit: nil)
|
|
44
78
|
# Raises because sevgi/derender is unavailable.
|
|
45
79
|
# @param file [String] source SVG/XML file
|
|
46
80
|
# @param id [String, Symbol] source node id
|
|
81
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
|
|
47
82
|
# @return [void]
|
|
48
83
|
# @raise [Sevgi::MissingComponentError] always
|
|
49
84
|
def Include(...) = MissingComponentError.("sevgi/derender")
|
|
@@ -5,6 +5,22 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# Inkscape-specific SVG DSL helpers.
|
|
7
7
|
module Inkscape
|
|
8
|
+
# Normalizes callable wrapper attributes outside the DSL method surface.
|
|
9
|
+
# @api private
|
|
10
|
+
module Wrapper
|
|
11
|
+
# Returns owned wrapper attributes with a stable default id when the module is named.
|
|
12
|
+
# @param mod [Module] callable drawing module
|
|
13
|
+
# @param attributes [Hash] wrapper attributes
|
|
14
|
+
# @return [Hash{Symbol => Object}] normalized attributes
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when attributes contains colliding names
|
|
16
|
+
def self.attributes(mod, attributes)
|
|
17
|
+
defaults = mod.name ? {id: F.demodulize(mod.name).to_sym} : {}
|
|
18
|
+
Attribute.defaults(attributes, **defaults)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private_constant :Wrapper
|
|
23
|
+
|
|
8
24
|
# Adds Inkscape template metadata.
|
|
9
25
|
# @param name [String] template name
|
|
10
26
|
# @param desc [String, nil] short description
|
|
@@ -23,41 +39,67 @@ module Sevgi
|
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
# Renders a callable module inside a group.
|
|
26
|
-
#
|
|
42
|
+
# Named modules default the group id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
43
|
+
# @example Supply a stable id for an anonymous callable
|
|
44
|
+
# drawing = Module.new do
|
|
45
|
+
# extend Sevgi::Graphics::Module
|
|
46
|
+
# def call = circle r: 5
|
|
47
|
+
# end
|
|
48
|
+
# Sevgi::Graphics.SVG(:inkscape) { Group drawing, attributes: {id: "drawing"} }
|
|
49
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
27
50
|
# @param args [Array<Object>] callable arguments
|
|
28
|
-
# @param
|
|
51
|
+
# @param attributes [Hash] group attributes; String and Symbol names are normalized and must not collide
|
|
52
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
53
|
+
# @yield forwards customization to the callable module
|
|
54
|
+
# @yieldreturn [Object] callable customization result
|
|
29
55
|
# @return [Sevgi::Graphics::Element] group element
|
|
30
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
31
|
-
def Group(mod, *args, **kwargs, &block)
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
57
|
+
def Group(mod, *args, attributes: {}, **kwargs, &block)
|
|
58
|
+
Graphics::Module.__send__(:callables, mod)
|
|
59
|
+
ArgumentError.("Group attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
60
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
61
|
+
g(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
# Renders a callable module inside an Inkscape layer.
|
|
37
|
-
#
|
|
65
|
+
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
66
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
38
67
|
# @param args [Array<Object>] callable arguments
|
|
39
|
-
# @param
|
|
68
|
+
# @param attributes [Hash] layer attributes; String and Symbol names are normalized and must not collide
|
|
69
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
70
|
+
# @yield forwards customization to the callable module
|
|
71
|
+
# @yieldreturn [Object] callable customization result
|
|
40
72
|
# @return [Sevgi::Graphics::Element] layer element
|
|
41
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
42
|
-
def Layer(mod, *args, **kwargs, &block)
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
74
|
+
def Layer(mod, *args, attributes: {}, **kwargs, &block)
|
|
75
|
+
Graphics::Module.__send__(:callables, mod)
|
|
76
|
+
ArgumentError.("Layer attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
77
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
78
|
+
layer(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
45
79
|
end
|
|
46
80
|
|
|
47
81
|
# Renders a callable module inside an insensitive Inkscape layer.
|
|
48
|
-
#
|
|
82
|
+
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
83
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
49
84
|
# @param args [Array<Object>] callable arguments
|
|
50
|
-
# @param
|
|
85
|
+
# @param attributes [Hash] layer attributes; String and Symbol names are normalized and must not collide
|
|
86
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
87
|
+
# @yield forwards customization to the callable module
|
|
88
|
+
# @yieldreturn [Object] callable customization result
|
|
51
89
|
# @return [Sevgi::Graphics::Element] layer element
|
|
52
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
53
|
-
def Layer!(mod, *args, **kwargs, &block)
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
91
|
+
def Layer!(mod, *args, attributes: {}, **kwargs, &block)
|
|
92
|
+
Graphics::Module.__send__(:callables, mod)
|
|
93
|
+
ArgumentError.("Layer attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
94
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
95
|
+
layer!(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
56
96
|
end
|
|
57
97
|
|
|
58
98
|
# @overload layer(**attributes)
|
|
59
99
|
# Builds an Inkscape layer group.
|
|
60
100
|
# @param attributes [Hash] layer attributes
|
|
101
|
+
# @yield evaluates the drawing DSL in the layer
|
|
102
|
+
# @yieldreturn [Object] ignored block result
|
|
61
103
|
# @return [Sevgi::Graphics::Element] layer group
|
|
62
104
|
def layer(**, &block)
|
|
63
105
|
g("inkscape:groupmode": "layer", **, &block)
|
|
@@ -66,51 +108,178 @@ module Sevgi
|
|
|
66
108
|
# @overload layer!(**attributes)
|
|
67
109
|
# Builds an insensitive Inkscape layer group.
|
|
68
110
|
# @param attributes [Hash] layer attributes
|
|
111
|
+
# @yield evaluates the drawing DSL in the layer
|
|
112
|
+
# @yieldreturn [Object] ignored block result
|
|
69
113
|
# @return [Sevgi::Graphics::Element] layer group
|
|
70
114
|
def layer!(**, &block)
|
|
71
115
|
layer("sodipodi:insensitive": "true", **, &block)
|
|
72
116
|
end
|
|
73
117
|
|
|
74
|
-
#
|
|
75
|
-
# @
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pages
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
# Validates and constructs Inkscape page collections outside the DSL method surface.
|
|
119
|
+
# @api private
|
|
120
|
+
module Pagination
|
|
121
|
+
class << self
|
|
122
|
+
# Builds a namedview from normalized page attributes.
|
|
123
|
+
# @param context [Sevgi::Graphics::Element] DSL element receiving the namedview
|
|
124
|
+
# @param pages [Array<Hash>] page attributes
|
|
125
|
+
# @param namedview [Hash] namedview attributes
|
|
126
|
+
# @param page [Hash] shared page attributes
|
|
127
|
+
# @return [Sevgi::Graphics::Element] namedview element
|
|
128
|
+
# @raise [Sevgi::ArgumentError] when an attribute channel or page is invalid
|
|
129
|
+
def call(context, pages, namedview:, page:, &block)
|
|
130
|
+
namedview, page = channels(namedview, page)
|
|
131
|
+
pages = pages.each_with_index.map { |attributes, index| normalize(attributes, page, index) }
|
|
132
|
+
|
|
133
|
+
context.Element(:"sodipodi:namedview", **namedview) do
|
|
134
|
+
pages.each do |attributes|
|
|
135
|
+
element = Element(:"inkscape:page", **attributes)
|
|
136
|
+
block&.call(element)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Generates validated attributes for a rectangular page grid.
|
|
142
|
+
# @return [Array<Hash{Symbol => Object}>] page attribute hashes
|
|
143
|
+
# @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
|
|
144
|
+
def tabular(rows:, cols:, width:, height:, gap:)
|
|
145
|
+
width, height, gap = normalize_grid(rows:, cols:, width:, height:, gap:)
|
|
146
|
+
|
|
147
|
+
rows.times.flat_map do |row|
|
|
148
|
+
cols.times.map do |col|
|
|
149
|
+
x = col * (width + gap)
|
|
150
|
+
y = row * (height + gap)
|
|
151
|
+
label = "#{row + 1}x#{col + 1}"
|
|
152
|
+
{id: "pageview-#{label}", x:, y:, width:, height:}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Validates and normalizes the namedview and page-default channels.
|
|
160
|
+
# @return [Array<Hash>] normalized namedview attributes and page defaults
|
|
161
|
+
# @raise [Sevgi::ArgumentError] when either channel is not a Hash or has invalid attributes
|
|
162
|
+
# @api private
|
|
163
|
+
def channels(namedview, page)
|
|
164
|
+
ArgumentError.("Namedview attributes must be a Hash") unless namedview.is_a?(::Hash)
|
|
165
|
+
ArgumentError.("Page attributes must be a Hash") unless page.is_a?(::Hash)
|
|
166
|
+
[Attribute.defaults(namedview, id: "namedview"), Attribute.normalize(page)]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Normalizes one explicit or generated page id.
|
|
170
|
+
# @return [Hash{String, Symbol => Object}] attributes beginning with a canonical id
|
|
171
|
+
# @raise [Sevgi::ArgumentError] when String and Symbol ids collide
|
|
172
|
+
# @api private
|
|
173
|
+
def identify(attributes, index)
|
|
174
|
+
id = attributes.key?(:id) ? attributes.delete(:id) : "page-#{index + 1}"
|
|
175
|
+
{id:}.merge(attributes)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Normalizes and validates one page.
|
|
179
|
+
# @return [Hash{String, Symbol => Object}] independent page attributes
|
|
180
|
+
# @raise [Sevgi::ArgumentError] when attributes or dimensions are invalid
|
|
181
|
+
# @api private
|
|
182
|
+
def normalize(attributes, defaults, index)
|
|
183
|
+
ArgumentError.("Page #{index + 1} must be a Hash") unless attributes.is_a?(::Hash)
|
|
184
|
+
attributes = defaults.merge(Attribute.normalize(attributes))
|
|
185
|
+
%i[x y].each { number(attributes, it, index) }
|
|
186
|
+
%i[width height].each do |field|
|
|
187
|
+
number(attributes, field, index, positive: true)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
identify(attributes, index)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Validates and normalizes page-grid arguments.
|
|
194
|
+
# @return [Array<(Integer, Float)>] normalized width, height, and gap
|
|
195
|
+
# @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
|
|
196
|
+
# @api private
|
|
197
|
+
def normalize_grid(rows:, cols:, width:, height:, gap:)
|
|
198
|
+
{rows:, cols:}.each do |field, count|
|
|
199
|
+
unless count.is_a?(::Integer) && count.positive?
|
|
200
|
+
ArgumentError.("Page #{field} must be a positive Integer")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
[
|
|
205
|
+
Scalar.number(width, context: "page", field: :width, positive: true),
|
|
206
|
+
Scalar.number(height, context: "page", field: :height, positive: true),
|
|
207
|
+
Scalar.number(gap, context: "page", field: :gap, nonnegative: true)
|
|
208
|
+
]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Replaces one page field with its normalized SVG number.
|
|
212
|
+
# @return [Integer, Float] normalized number
|
|
213
|
+
# @api private
|
|
214
|
+
def number(attributes, field, index, **range)
|
|
215
|
+
key = attributes.key?(field) ? field : field.to_s
|
|
216
|
+
attributes[key] = Scalar.number(value(attributes, field, index), context: "page", field:, **range)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Returns one required page field.
|
|
220
|
+
# @return [Object] page field value
|
|
221
|
+
# @raise [Sevgi::ArgumentError] when the field is absent
|
|
222
|
+
# @api private
|
|
223
|
+
def value(attributes, field, index)
|
|
224
|
+
attributes.fetch(field) do
|
|
225
|
+
attributes.fetch(field.to_s) { ArgumentError.("Page #{index + 1} requires #{field}") }
|
|
226
|
+
end
|
|
85
227
|
end
|
|
86
228
|
end
|
|
87
229
|
end
|
|
88
230
|
|
|
231
|
+
private_constant :Pagination
|
|
232
|
+
|
|
233
|
+
# Builds an Inkscape namedview containing page elements.
|
|
234
|
+
# @example Build explicit pages with separate namedview and page attributes
|
|
235
|
+
# Sevgi::Graphics.SVG(:inkscape) do
|
|
236
|
+
# Pages(
|
|
237
|
+
# {x: 0, y: 0, width: 100, height: 50, label: "front"},
|
|
238
|
+
# namedview: {id: "views"},
|
|
239
|
+
# page: {class: "print"}
|
|
240
|
+
# )
|
|
241
|
+
# end
|
|
242
|
+
# @param pages [Array<Hash>] page attribute hashes; numeric page fields are normalized to SVG numbers
|
|
243
|
+
# @param namedview [Hash] namedview attributes; String and Symbol names are normalized and must not collide
|
|
244
|
+
# @param page [Hash] page defaults; normalized page attributes override these defaults by name
|
|
245
|
+
# @yield [page] customizes each generated page element
|
|
246
|
+
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
247
|
+
# @yieldreturn [Object] ignored customization result
|
|
248
|
+
# @return [Sevgi::Graphics::Element] namedview element
|
|
249
|
+
# @raise [Sevgi::ArgumentError] when an attribute channel, page, coordinate, or dimension is invalid
|
|
250
|
+
def Pages(*pages, namedview: {}, page: {}, &block) = Pagination.call(self, pages, namedview:, page:, &block)
|
|
251
|
+
|
|
89
252
|
# Builds a tabular set of Inkscape pages.
|
|
253
|
+
# @example Build a page grid using the same attribute channels as {#Pages}
|
|
254
|
+
# Sevgi::Graphics.SVG(:inkscape) do
|
|
255
|
+
# PagesTabular(
|
|
256
|
+
# rows: 2, cols: 3, width: 100, height: 50, gap: 5,
|
|
257
|
+
# namedview: {id: "views"}, page: {class: "print"}
|
|
258
|
+
# )
|
|
259
|
+
# end
|
|
90
260
|
# @param rows [Integer] number of rows
|
|
91
261
|
# @param cols [Integer] number of columns
|
|
92
|
-
# @param width [Numeric] page width
|
|
93
|
-
# @param height [Numeric] page height
|
|
94
|
-
# @param gap [Numeric] gap
|
|
95
|
-
# @param
|
|
96
|
-
# @
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
end
|
|
107
|
-
end
|
|
262
|
+
# @param width [Numeric] finite positive page width, normalized to an SVG number
|
|
263
|
+
# @param height [Numeric] finite positive page height, normalized to an SVG number
|
|
264
|
+
# @param gap [Numeric] finite non-negative gap, normalized to an SVG number
|
|
265
|
+
# @param namedview [Hash] namedview attributes; String and Symbol names are normalized and must not collide
|
|
266
|
+
# @param page [Hash] page defaults; normalized page attributes override these defaults by name
|
|
267
|
+
# @yield [page] customizes each generated page element
|
|
268
|
+
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
269
|
+
# @yieldreturn [Object] ignored customization result
|
|
270
|
+
# @return [Sevgi::Graphics::Element] namedview element
|
|
271
|
+
# @raise [Sevgi::ArgumentError] when counts, dimensions, gap, or an attribute channel is invalid
|
|
272
|
+
# @see #Pages
|
|
273
|
+
def PagesTabular(rows:, cols:, width:, height:, gap:, namedview: {}, page: {}, &block)
|
|
274
|
+
pages = Pagination.tabular(rows:, cols:, width:, height:, gap:)
|
|
275
|
+
Pages(*pages, namedview:, page:, &block)
|
|
108
276
|
end
|
|
109
277
|
|
|
110
|
-
# Internal symbol which does not show up Symbols Menu
|
|
111
278
|
# @overload symbol!(**attributes)
|
|
112
279
|
# Builds an Inkscape symbol group hidden from the symbols menu.
|
|
113
280
|
# @param attributes [Hash] symbol attributes
|
|
281
|
+
# @yield evaluates the drawing DSL in the symbol group
|
|
282
|
+
# @yieldreturn [Object] ignored block result
|
|
114
283
|
# @return [Sevgi::Graphics::Element] symbol group
|
|
115
284
|
def symbol!(**, &block)
|
|
116
285
|
if Is?(:defs)
|