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
|
@@ -9,7 +9,9 @@ module Sevgi
|
|
|
9
9
|
#
|
|
10
10
|
# @!attribute [r] value
|
|
11
11
|
# @return [Object] value returned from traversal
|
|
12
|
-
Stop = Data.define(:value)
|
|
12
|
+
Stop = Data.define(:value) do
|
|
13
|
+
private_class_method :[], :new
|
|
14
|
+
end
|
|
13
15
|
|
|
14
16
|
# Internal traversal engine.
|
|
15
17
|
# @api private
|
|
@@ -62,7 +64,7 @@ module Sevgi
|
|
|
62
64
|
insertion = Adoption.index_for(self, new_parent, index)
|
|
63
65
|
|
|
64
66
|
self.Orphan()
|
|
65
|
-
(
|
|
67
|
+
Element.send(:attach, self, new_parent, index: insertion)
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -78,6 +80,12 @@ module Sevgi
|
|
|
78
80
|
|
|
79
81
|
# Appends distinct existing elements as children in argument order.
|
|
80
82
|
# Each element transfers from its current parent. The complete batch is validated before any element moves.
|
|
83
|
+
# @example Move existing elements into a group
|
|
84
|
+
# Sevgi::Graphics.SVG(:minimal) do
|
|
85
|
+
# dot = circle r: 2
|
|
86
|
+
# label = text "Ready", x: 6
|
|
87
|
+
# g(id: "status").Append(dot, label)
|
|
88
|
+
# end
|
|
81
89
|
# @param elements [Array<Sevgi::Graphics::Element>] distinct elements to append
|
|
82
90
|
# @return [Sevgi::Graphics::Element] self
|
|
83
91
|
# @raise [Sevgi::ArgumentError] when an argument has a different element class, is repeated, or is this target or its ancestor
|
|
@@ -127,7 +135,8 @@ module Sevgi
|
|
|
127
135
|
# @return [Sevgi::Graphics::Element] new child element
|
|
128
136
|
# @raise [Sevgi::ArgumentError] when the tag, attributes, or content are not valid XML
|
|
129
137
|
def Element(tag, *contents, **attributes, &block)
|
|
130
|
-
|
|
138
|
+
contents.map! { it.is_a?(Content) ? it : Content.encoded(it) }
|
|
139
|
+
self.class.send(:new, tag, contents:, attributes:, parent: self, &block)
|
|
131
140
|
end
|
|
132
141
|
|
|
133
142
|
# Forwards this element as the first argument to another receiver.
|
|
@@ -148,10 +157,13 @@ module Sevgi
|
|
|
148
157
|
self.name() == name.to_sym
|
|
149
158
|
end
|
|
150
159
|
|
|
151
|
-
# Removes this element from its parent.
|
|
152
|
-
# @return [Sevgi::Graphics::Element, nil]
|
|
160
|
+
# Removes this element from its parent and makes it a detached subtree root.
|
|
161
|
+
# @return [Sevgi::Graphics::Element, nil] self, or nil for root elements
|
|
153
162
|
def Orphan
|
|
154
|
-
|
|
163
|
+
return if Root?()
|
|
164
|
+
|
|
165
|
+
Element.send(:detach, self)
|
|
166
|
+
self
|
|
155
167
|
end
|
|
156
168
|
|
|
157
169
|
# Prepends distinct existing elements as children in argument order.
|
|
@@ -163,11 +175,12 @@ module Sevgi
|
|
|
163
175
|
tap { Adoption.batch(elements, self, front: true) }
|
|
164
176
|
end
|
|
165
177
|
|
|
166
|
-
# Returns the
|
|
167
|
-
#
|
|
178
|
+
# Returns the topmost element in this tree.
|
|
179
|
+
# A detached subtree returns its detached topmost element; use {#Root?} to distinguish a document root.
|
|
180
|
+
# @return [Sevgi::Graphics::Element] document root or detached topmost element
|
|
168
181
|
def Root
|
|
169
182
|
element = self
|
|
170
|
-
element = element.parent
|
|
183
|
+
element = element.parent while element.parent
|
|
171
184
|
|
|
172
185
|
element
|
|
173
186
|
end
|
|
@@ -175,16 +188,20 @@ module Sevgi
|
|
|
175
188
|
# Reports whether this element is the root document element.
|
|
176
189
|
# @return [Boolean]
|
|
177
190
|
def Root?
|
|
178
|
-
|
|
191
|
+
Element.root?(self)
|
|
179
192
|
end
|
|
180
193
|
|
|
181
194
|
# @overload Stay(value)
|
|
182
195
|
# Wraps a traversal return value as a stop token.
|
|
183
196
|
# @param value [Object] value returned from traversal
|
|
184
197
|
# @return [Sevgi::Graphics::Mixtures::Stop]
|
|
185
|
-
def Stay(...) = Stop.new
|
|
198
|
+
def Stay(...) = Stop.send(:new, ...)
|
|
186
199
|
|
|
187
200
|
# Traverses the subtree depth-first.
|
|
201
|
+
# @example Find the first circle and stop the traversal
|
|
202
|
+
# drawing = Sevgi::Graphics.SVG(:minimal) { g { circle id: "target"; circle id: "later" } }
|
|
203
|
+
# found = drawing.Traverse { |node| node.Stay(node) if node.Is? :circle }
|
|
204
|
+
# found[:id] # => "target"
|
|
188
205
|
# @param depth [Integer] starting depth
|
|
189
206
|
# @param leave [Proc, nil] optional leave callback
|
|
190
207
|
# @yield [element, depth] visits each element before its children
|
|
@@ -215,7 +232,7 @@ module Sevgi
|
|
|
215
232
|
loop do
|
|
216
233
|
yield(element, height).tap { return it.value if it.is_a?(Stop) }
|
|
217
234
|
|
|
218
|
-
break
|
|
235
|
+
break unless element.parent
|
|
219
236
|
|
|
220
237
|
element = element.parent
|
|
221
238
|
height += 1
|
|
@@ -223,28 +240,50 @@ module Sevgi
|
|
|
223
240
|
end
|
|
224
241
|
|
|
225
242
|
# Evaluates a block in the parent element context.
|
|
226
|
-
# @
|
|
243
|
+
# @example Add a sibling while forwarding its id
|
|
244
|
+
# root = Sevgi::Graphics.SVG id: "root"
|
|
245
|
+
# child = root.g id: "child"
|
|
246
|
+
# child.With("sibling") { |id| line id: }
|
|
247
|
+
# @param args [Array<Object>] positional arguments passed to the block
|
|
248
|
+
# @param receiver [Sevgi::Graphics::Element] element whose parent becomes the block receiver
|
|
227
249
|
# @param kwargs [Hash] keyword arguments passed to the block
|
|
228
|
-
# @yield evaluates in the selected
|
|
250
|
+
# @yield [*args, **kwargs] evaluates in the selected element's parent context
|
|
229
251
|
# @yieldreturn [Object] ignored block result
|
|
230
252
|
# @return [Sevgi::Graphics::Element] self
|
|
231
|
-
|
|
232
|
-
|
|
253
|
+
# @raise [Sevgi::ArgumentError] when no block is given
|
|
254
|
+
# @raise [Sevgi::ArgumentError] when receiver is not an element or has no parent
|
|
255
|
+
def With(*args, receiver: self, **kwargs, &block)
|
|
256
|
+
ArgumentError.("Block required") unless block
|
|
257
|
+
ArgumentError.("Receiver must be an element") unless receiver.is_a?(Element)
|
|
258
|
+
|
|
259
|
+
parent = receiver.parent
|
|
260
|
+
ArgumentError.("Receiver has no parent") unless parent
|
|
261
|
+
|
|
262
|
+
tap { parent.instance_exec(*args, **kwargs, &block) }
|
|
233
263
|
end
|
|
234
264
|
|
|
235
265
|
# Evaluates a block in this element context.
|
|
236
|
-
# @
|
|
266
|
+
# @example Select a receiver without consuming the block argument
|
|
267
|
+
# target = Sevgi::Graphics.SVG id: "target"
|
|
268
|
+
# source = Sevgi::Graphics.SVG id: "source"
|
|
269
|
+
# source.Within("child", receiver: target) { |id| g id: }
|
|
270
|
+
# @param args [Array<Object>] positional arguments passed to the block
|
|
271
|
+
# @param receiver [Object] block receiver
|
|
237
272
|
# @param kwargs [Hash] keyword arguments passed to the block
|
|
238
|
-
# @yield evaluates in the selected receiver context
|
|
273
|
+
# @yield [*args, **kwargs] evaluates in the selected receiver context
|
|
239
274
|
# @yieldreturn [Object] ignored block result
|
|
240
275
|
# @return [Sevgi::Graphics::Element] self
|
|
241
|
-
|
|
242
|
-
|
|
276
|
+
# @raise [Sevgi::ArgumentError] when no block is given
|
|
277
|
+
def Within(*args, receiver: self, **kwargs, &block)
|
|
278
|
+
ArgumentError.("Block required") unless block
|
|
279
|
+
|
|
280
|
+
tap { receiver.instance_exec(*args, **kwargs, &block) }
|
|
243
281
|
end
|
|
244
282
|
|
|
245
283
|
# Appends an element as a child.
|
|
246
284
|
# @param element [Sevgi::Graphics::Element] element to append
|
|
247
285
|
# @return [Sevgi::Graphics::Element] self
|
|
286
|
+
# @raise [Sevgi::ArgumentError] when the element has a different concrete class or would create a tree cycle
|
|
248
287
|
def <<(element)
|
|
249
288
|
Append(element)
|
|
250
289
|
end
|
|
@@ -6,23 +6,31 @@ module Sevgi
|
|
|
6
6
|
# DSL helpers for duplicating independent element subtrees.
|
|
7
7
|
module Duplicate
|
|
8
8
|
# Duplicates an element subtree as an independent tree and optionally translates it.
|
|
9
|
-
# Copied elements receive new child arrays, attribute stores, and content arrays.
|
|
10
|
-
# moved to
|
|
11
|
-
# ids without rendering
|
|
12
|
-
#
|
|
13
|
-
# @param
|
|
14
|
-
# @param
|
|
9
|
+
# Copied elements receive new child arrays, attribute stores, and content arrays. Visible `id` attributes are
|
|
10
|
+
# moved to non-rendering `-id` metadata before the optional block runs, allowing the block to derive replacement
|
|
11
|
+
# ids without rendering duplicates. A pre-existing `-id` takes precedence over the visible id.
|
|
12
|
+
# Translation and parent channels are validated before the subtree is copied or the customization block runs.
|
|
13
|
+
# @param dx [Numeric, nil] finite x translation; nil omits the axis
|
|
14
|
+
# @param dy [Numeric, nil] finite y translation; nil omits the axis
|
|
15
|
+
# @param parent [Sevgi::Graphics::Element, nil] explicit parent, or the source parent when nil
|
|
15
16
|
# @yield [element] optional customization hook for each copied element
|
|
16
17
|
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
17
18
|
# @yieldreturn [Object] ignored customization result
|
|
18
19
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
19
20
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when a translation is not a finite real number
|
|
20
22
|
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
23
|
+
# @example Remap source ids on a duplicate
|
|
24
|
+
# source = Sevgi::Graphics.SVG { rect id: "shape" }.children.first
|
|
25
|
+
# copy = source.Duplicate do |node|
|
|
26
|
+
# node[:id] = "#{node[:"-id"]}-copy" if node[:"-id"]
|
|
27
|
+
# end
|
|
21
28
|
def Duplicate(dx: nil, dy: nil, parent: nil, &block)
|
|
29
|
+
dx, dy, target = Subtree.channels(self, dx, dy, parent)
|
|
22
30
|
duplicated = Subtree.copy(self)
|
|
23
31
|
Subtree.prepare(duplicated, &block)
|
|
24
32
|
Subtree.translate(duplicated, dx, dy)
|
|
25
|
-
Subtree.attach(duplicated,
|
|
33
|
+
Subtree.attach(duplicated, target)
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
# Duplicates an element subtree along the x-axis.
|
|
@@ -33,8 +41,12 @@ module Sevgi
|
|
|
33
41
|
# @yieldreturn [Object] ignored customization result
|
|
34
42
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
35
43
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
44
|
+
# @raise [Sevgi::ArgumentError] when dx is not a finite real number
|
|
36
45
|
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
37
|
-
def DuplicateX(dx, parent: nil, &block)
|
|
46
|
+
def DuplicateX(dx, parent: nil, &block)
|
|
47
|
+
ArgumentError.("Duplicate x translation cannot be nil") if dx.nil?
|
|
48
|
+
Duplicate(dx:, dy: 0, parent:, &block)
|
|
49
|
+
end
|
|
38
50
|
|
|
39
51
|
# Duplicates an element subtree along the y-axis.
|
|
40
52
|
# @param dy [Numeric] y translation
|
|
@@ -44,17 +56,35 @@ module Sevgi
|
|
|
44
56
|
# @yieldreturn [Object] ignored customization result
|
|
45
57
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
46
58
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
59
|
+
# @raise [Sevgi::ArgumentError] when dy is not a finite real number
|
|
47
60
|
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
48
|
-
def DuplicateY(dy, parent: nil, &block)
|
|
61
|
+
def DuplicateY(dy, parent: nil, &block)
|
|
62
|
+
ArgumentError.("Duplicate y translation cannot be nil") if dy.nil?
|
|
63
|
+
Duplicate(dx: 0, dy:, parent:, &block)
|
|
64
|
+
end
|
|
49
65
|
|
|
50
66
|
# Recursive subtree copier that keeps duplicate implementation state out of the DSL surface.
|
|
51
67
|
# @api private
|
|
52
68
|
module Subtree
|
|
69
|
+
# Validates and normalizes duplicate option channels before copying.
|
|
70
|
+
# @return [Array<(Integer, Float, Sevgi::Graphics::Element, nil)>] normalized dx, dy, and target parent
|
|
71
|
+
# @raise [Sevgi::ArgumentError] when a translation or parent is invalid
|
|
72
|
+
def self.channels(source, dx, dy, parent)
|
|
73
|
+
target = parent.nil? ? source.parent : parent
|
|
74
|
+
unless target.nil? || source.instance_of?(target.class)
|
|
75
|
+
ArgumentError.("Element type does not match the new parent type: #{source.class}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
dx = Scalar.number(dx, context: "duplicate translation", field: :x) unless dx.nil?
|
|
79
|
+
dy = Scalar.number(dy, context: "duplicate translation", field: :y) unless dy.nil?
|
|
80
|
+
[dx, dy, target]
|
|
81
|
+
end
|
|
82
|
+
|
|
53
83
|
# Builds an independent copy of an element subtree.
|
|
54
84
|
# @param element [Sevgi::Graphics::Element] source subtree root
|
|
55
85
|
# @param parent [Sevgi::Graphics::Element, Object] parent for the copied root
|
|
56
86
|
# @return [Sevgi::Graphics::Element] copied subtree root
|
|
57
|
-
def self.copy(element, parent = element
|
|
87
|
+
def self.copy(element, parent = Element.send(:tree_parent, element))
|
|
58
88
|
element.dup.tap do |duplicated|
|
|
59
89
|
duplicated.send(:parent=, parent)
|
|
60
90
|
duplicated.send(:attributes=, element.attributes.dup)
|
|
@@ -67,8 +97,12 @@ module Sevgi
|
|
|
67
97
|
# @api private
|
|
68
98
|
def self.prepare(element, &block)
|
|
69
99
|
element.Traverse() do |node|
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
if node.attributes.has?(:id)
|
|
101
|
+
id = node.attributes.delete(:id)
|
|
102
|
+
metadata = :"#{Attributes::META_PREFIX}id"
|
|
103
|
+
node[metadata] = id unless node.attributes.has?(metadata)
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
block&.call(node)
|
|
73
107
|
end
|
|
74
108
|
end
|
|
@@ -76,13 +110,13 @@ module Sevgi
|
|
|
76
110
|
# Applies an optional translation to a copied subtree.
|
|
77
111
|
# @api private
|
|
78
112
|
def self.translate(element, dx, dy)
|
|
79
|
-
element.Translate(dx, dy)
|
|
113
|
+
element.Translate(dx || 0, dy) unless dx.nil? && dy.nil?
|
|
80
114
|
end
|
|
81
115
|
|
|
82
116
|
# Attaches a copied subtree unless it is a detached root copy.
|
|
83
117
|
# @api private
|
|
84
|
-
def self.attach(element,
|
|
85
|
-
element.Adopt(
|
|
118
|
+
def self.attach(element, target)
|
|
119
|
+
element.Adopt(target) if target
|
|
86
120
|
element
|
|
87
121
|
end
|
|
88
122
|
end
|
|
@@ -5,17 +5,48 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for native SVG export formats.
|
|
7
7
|
module Export
|
|
8
|
+
# Validates the closed export option channel before optional components load or rendering starts.
|
|
9
|
+
# @api private
|
|
10
|
+
module Options
|
|
11
|
+
KEYS = %i[css default dpi height width].freeze
|
|
12
|
+
private_constant :KEYS
|
|
13
|
+
|
|
14
|
+
# Returns validated export options.
|
|
15
|
+
# @param options [Hash] export options
|
|
16
|
+
# @return [Hash] original options
|
|
17
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
18
|
+
def self.call(options)
|
|
19
|
+
unknown = options.keys - KEYS
|
|
20
|
+
ArgumentError.("Unknown export option: #{unknown.first}") unless unknown.empty?
|
|
21
|
+
options
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_constant :Options
|
|
26
|
+
|
|
8
27
|
# Exports the document as PDF.
|
|
9
|
-
#
|
|
28
|
+
# Relative paths are expanded, missing parent directories are created after export validation, and an existing
|
|
29
|
+
# file is replaced. An existing directory target uses the caller-derived default PDF name.
|
|
30
|
+
# @param path [String, #to_path, nil] output path or existing directory
|
|
10
31
|
# @param kwargs [Hash] export options
|
|
32
|
+
# @option kwargs [String, #to_path, nil] :default caller-derived output name used when path is nil or a directory
|
|
33
|
+
# @option kwargs [Numeric, nil] :width finite positive target width in CSS pixels
|
|
34
|
+
# @option kwargs [Numeric, nil] :height finite positive target height in CSS pixels
|
|
35
|
+
# @option kwargs [Numeric] :dpi (96.0) finite positive CSS pixel density
|
|
36
|
+
# @option kwargs [String, nil] :css CSS inserted before rendering
|
|
11
37
|
# @yield [svg] transforms SVG source before rendering
|
|
12
38
|
# @yieldparam svg [String] rendered SVG source
|
|
13
39
|
# @yieldreturn [String] transformed SVG source
|
|
14
|
-
# @return [String] output path
|
|
40
|
+
# @return [String] expanded output path
|
|
41
|
+
# @raise [Sevgi::ArgumentError] when a path, default, option name, CSS value, or transformed SVG is invalid
|
|
42
|
+
# @raise [Sevgi::ValidationError] when validation is enabled and the document violates the SVG standard
|
|
43
|
+
# @raise [Sevgi::Graphics::LintError] when linting is enabled and the document has structural conflicts
|
|
15
44
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
16
45
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
17
46
|
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
47
|
+
# @raise [SystemCallError] when the output directory or file cannot be created or written
|
|
18
48
|
def PDF(path = nil, **kwargs, &block)
|
|
49
|
+
kwargs = Options.(kwargs)
|
|
19
50
|
begin
|
|
20
51
|
require "sevgi/sundries"
|
|
21
52
|
|
|
@@ -29,16 +60,28 @@ module Sevgi
|
|
|
29
60
|
end
|
|
30
61
|
|
|
31
62
|
# Exports the document as PNG.
|
|
32
|
-
#
|
|
63
|
+
# Relative paths are expanded, missing parent directories are created after export validation, and an existing
|
|
64
|
+
# file is replaced. An existing directory target uses the caller-derived default PNG name.
|
|
65
|
+
# @param path [String, #to_path, nil] output path or existing directory
|
|
33
66
|
# @param kwargs [Hash] export options
|
|
67
|
+
# @option kwargs [String, #to_path, nil] :default caller-derived output name used when path is nil or a directory
|
|
68
|
+
# @option kwargs [Numeric, nil] :width finite positive target width in output pixels
|
|
69
|
+
# @option kwargs [Numeric, nil] :height finite positive target height in output pixels
|
|
70
|
+
# @option kwargs [Numeric] :dpi (96.0) finite positive CSS pixel density
|
|
71
|
+
# @option kwargs [String, nil] :css CSS inserted before rendering
|
|
34
72
|
# @yield [svg] transforms SVG source before rendering
|
|
35
73
|
# @yieldparam svg [String] rendered SVG source
|
|
36
74
|
# @yieldreturn [String] transformed SVG source
|
|
37
|
-
# @return [String] output path
|
|
75
|
+
# @return [String] expanded output path
|
|
76
|
+
# @raise [Sevgi::ArgumentError] when a path, default, option name, CSS value, or transformed SVG is invalid
|
|
77
|
+
# @raise [Sevgi::ValidationError] when validation is enabled and the document violates the SVG standard
|
|
78
|
+
# @raise [Sevgi::Graphics::LintError] when linting is enabled and the document has structural conflicts
|
|
38
79
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
39
80
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
40
81
|
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
82
|
+
# @raise [SystemCallError] when the output directory or file cannot be created or written
|
|
41
83
|
def PNG(path = nil, **kwargs, &block)
|
|
84
|
+
kwargs = Options.(kwargs)
|
|
42
85
|
begin
|
|
43
86
|
require "sevgi/sundries"
|
|
44
87
|
|
|
@@ -54,15 +97,12 @@ module Sevgi
|
|
|
54
97
|
private
|
|
55
98
|
|
|
56
99
|
def Export(path = nil, default: nil, **kwargs, &block)
|
|
57
|
-
default
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
else
|
|
62
|
-
default
|
|
63
|
-
end => path
|
|
100
|
+
if default.nil?
|
|
101
|
+
extension = kwargs[:format] ? ".#{kwargs[:format]}" : ".pdf"
|
|
102
|
+
default = F.subext(extension, caller_locations(2..2).first.path)
|
|
103
|
+
end
|
|
64
104
|
|
|
65
|
-
|
|
105
|
+
path = Path.resolve(path, default:, context: "Export")
|
|
66
106
|
|
|
67
107
|
Sundries::Export.(call, path, **kwargs, &block)
|
|
68
108
|
end
|
|
@@ -3,24 +3,65 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
-
# DSL helpers for drawing geometry-derived hatch lines.
|
|
6
|
+
# DSL helpers for drawing geometry values and geometry-derived hatch lines.
|
|
7
|
+
#
|
|
8
|
+
# `Draw` delegates to each geometry object's drawing protocol. `Hatch`
|
|
9
|
+
# first sweeps interior spans through a closed lined element, then draws
|
|
10
|
+
# them. Its `angle` describes line direction; `step` is perpendicular
|
|
11
|
+
# spacing. The default initial line passes through `element.position`.
|
|
12
|
+
# The built-in `:inkscape` document profile includes this mixture;
|
|
13
|
+
# `:minimal`, `:default`, and `:html` do not.
|
|
14
|
+
#
|
|
15
|
+
# Hatch materializes separate finite SVG path elements. When only a repeated visual fill matters, use an SVG
|
|
16
|
+
# pattern and leave repetition and clipping to the renderer instead of computing line geometry.
|
|
17
|
+
# @example Add geometry drawing to a scoped custom profile
|
|
18
|
+
# profile = Class.new(Sevgi::Graphics::Document::Base)
|
|
19
|
+
# Sevgi::Graphics::Mixtures.mixin(:Hatch, profile)
|
|
20
|
+
# region = Sevgi::Geometry::Rect[24, 12]
|
|
21
|
+
# Sevgi::Graphics.SVG(profile) do
|
|
22
|
+
# Draw region.lines, stroke: "silver"
|
|
23
|
+
# Hatch region, angle: 30, step: 3, stroke: "black"
|
|
24
|
+
# end.Render
|
|
25
|
+
# @see Sevgi::Geometry::Operation.sweep
|
|
26
|
+
# @see https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/pattern SVG pattern element
|
|
7
27
|
module Hatch
|
|
8
28
|
# Draws one or more geometry line-like objects into this element.
|
|
9
|
-
# @
|
|
29
|
+
# @example Draw geometry lines into a library-built document
|
|
30
|
+
# lines = [
|
|
31
|
+
# Sevgi::Geometry::Line.([0, 0], [20, 0]),
|
|
32
|
+
# Sevgi::Geometry::Line.([0, 5], [20, 5])
|
|
33
|
+
# ]
|
|
34
|
+
# drawing = Sevgi::Graphics.SVG(:inkscape) { Draw lines, stroke: "silver" }
|
|
35
|
+
# drawing.Render
|
|
36
|
+
# @param lines [#draw, Array<#draw>] drawable geometry objects
|
|
10
37
|
# @param kwargs [Hash] SVG attributes passed to each draw call
|
|
11
|
-
# @return [Array<Sevgi::Graphics::Element>] rendered
|
|
38
|
+
# @return [Array<Sevgi::Graphics::Element>] rendered SVG elements
|
|
12
39
|
def Draw(lines, **kwargs)
|
|
13
40
|
Array(lines).map { it.draw(self, **kwargs) }
|
|
14
41
|
end
|
|
15
42
|
|
|
16
43
|
# Draws hatch lines swept through a geometry element.
|
|
17
|
-
# @
|
|
44
|
+
# @example Hatch a closed geometry shape
|
|
45
|
+
# region = Sevgi::Geometry::Rect[24, 12, position: [2, 2]]
|
|
46
|
+
# drawing = Sevgi::Graphics.SVG(:inkscape) do
|
|
47
|
+
# Hatch region, angle: 30, step: 3, stroke: "black"
|
|
48
|
+
# end
|
|
49
|
+
# drawing.Render
|
|
50
|
+
# @example Control the first sweep line explicitly
|
|
51
|
+
# region = Sevgi::Geometry::Rect[24, 12, position: [2, 2]]
|
|
52
|
+
# Sevgi::Graphics.SVG(:inkscape) do
|
|
53
|
+
# Hatch region, initial: [2, 8], angle: 0, step: 3
|
|
54
|
+
# end
|
|
55
|
+
# @param element [Sevgi::Geometry::Element::Lined] lined geometry element to sweep
|
|
18
56
|
# @param angle [Numeric] hatch angle in degrees
|
|
19
57
|
# @param step [Numeric] distance between hatch lines
|
|
20
|
-
# @param initial [
|
|
58
|
+
# @param initial [Sevgi::Geometry::Point, Array<Numeric>, nil] initial sweep point, or nil for element.position
|
|
21
59
|
# @param kwargs [Hash] SVG attributes passed to each draw call
|
|
22
|
-
# @return [Array<Sevgi::Graphics::Element>] rendered
|
|
60
|
+
# @return [Array<Sevgi::Graphics::Element>] rendered hatch path elements
|
|
23
61
|
# @raise [Sevgi::MissingComponentError] when sevgi/geometry is unavailable
|
|
62
|
+
# @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
63
|
+
# @raise [Sevgi::Geometry::Error] when initial, angle, or step is invalid
|
|
64
|
+
# @raise [Sevgi::Geometry::Operation::OperationError] when no hatch lines are found or iteration reaches the limit
|
|
24
65
|
def Hatch(element, angle:, step:, initial: nil, **kwargs)
|
|
25
66
|
begin
|
|
26
67
|
require "sevgi/geometry"
|
|
@@ -31,7 +72,8 @@ module Sevgi
|
|
|
31
72
|
MissingComponentError.("sevgi/geometry")
|
|
32
73
|
end
|
|
33
74
|
|
|
34
|
-
|
|
75
|
+
initial = element.position if initial.nil? && element.is_a?(Geometry::Element::Lined)
|
|
76
|
+
Draw(Geometry::Operation.sweep!(element, initial:, angle:, step:), **kwargs)
|
|
35
77
|
end
|
|
36
78
|
end
|
|
37
79
|
end
|
|
@@ -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
|
|
@@ -5,21 +5,31 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for including derendered SVG/XML fragments.
|
|
7
7
|
#
|
|
8
|
-
# @!method Include(file, id)
|
|
8
|
+
# @!method Include(file, id, omit: nil)
|
|
9
9
|
# Includes a derendered node matching an id.
|
|
10
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
|
|
11
15
|
# @param file [String] source SVG/XML file
|
|
12
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
|
|
13
19
|
# @return [Sevgi::Graphics::Element, nil] included element, or nil when it produces no graphics output
|
|
14
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
|
|
15
22
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
16
|
-
# @!method IncludeChildren(file, id)
|
|
23
|
+
# @!method IncludeChildren(file, id, omit: nil)
|
|
17
24
|
# Includes the children of a derendered node matching an id.
|
|
18
25
|
# SVG/XML content is treated as data and is not evaluated as Ruby source.
|
|
19
26
|
# @param file [String] source SVG/XML file
|
|
20
27
|
# @param id [String, Symbol] source node id
|
|
21
|
-
# @
|
|
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
|
|
22
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
|
|
23
33
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
24
34
|
module Include
|
|
25
35
|
require "sevgi/derender"
|
|
@@ -29,38 +39,46 @@ module Sevgi
|
|
|
29
39
|
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
30
40
|
# @param file [String] source SVG/XML file
|
|
31
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
|
|
32
44
|
# @return [Sevgi::Graphics::Element, nil] included element, or nil when the selected node produces no graphics
|
|
33
45
|
# output
|
|
34
46
|
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
35
47
|
# absent
|
|
48
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
36
49
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
37
|
-
def Include(file, id) = Derender.evaluate_file(file, self, id:)
|
|
50
|
+
def Include(file, id, omit: nil) = Derender.evaluate_file(file, self, id:, omit:)
|
|
38
51
|
|
|
39
52
|
# Includes the children of a derendered node matching an id.
|
|
40
53
|
#
|
|
41
54
|
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
42
55
|
# @param file [String] source SVG/XML file
|
|
43
56
|
# @param id [String, Symbol] source node id
|
|
44
|
-
# @
|
|
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
|
|
45
60
|
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
46
61
|
# absent
|
|
62
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
47
63
|
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
48
|
-
def IncludeChildren(file, id) = Derender.
|
|
64
|
+
def IncludeChildren(file, id, omit: nil) = Derender.evaluate_children_file(file, self, id:, omit:)
|
|
49
65
|
rescue ::LoadError => e
|
|
50
66
|
raise unless e.path == "sevgi/derender"
|
|
51
67
|
|
|
52
|
-
# @overload IncludeChildren(file, id)
|
|
68
|
+
# @overload IncludeChildren(file, id, omit: nil)
|
|
53
69
|
# Raises because sevgi/derender is unavailable.
|
|
54
70
|
# @param file [String] source SVG/XML file
|
|
55
71
|
# @param id [String, Symbol] source node id
|
|
72
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
|
|
56
73
|
# @return [void]
|
|
57
74
|
# @raise [Sevgi::MissingComponentError] always
|
|
58
75
|
def IncludeChildren(...) = MissingComponentError.("sevgi/derender")
|
|
59
76
|
|
|
60
|
-
# @overload Include(file, id)
|
|
77
|
+
# @overload Include(file, id, omit: nil)
|
|
61
78
|
# Raises because sevgi/derender is unavailable.
|
|
62
79
|
# @param file [String] source SVG/XML file
|
|
63
80
|
# @param id [String, Symbol] source node id
|
|
81
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
|
|
64
82
|
# @return [void]
|
|
65
83
|
# @raise [Sevgi::MissingComponentError] always
|
|
66
84
|
def Include(...) = MissingComponentError.("sevgi/derender")
|