sevgi-graphics 0.98.2 → 1.1.0
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 +115 -1
- data/README.md +55 -1
- data/lib/sevgi/graphics/attribute.rb +15 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
- data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
- data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
- data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +4 -9
- data/lib/sevgi/graphics/auxiliary.rb +1 -2
- data/lib/sevgi/graphics/document/base.rb +3 -2
- data/lib/sevgi/graphics/document.rb +15 -27
- data/lib/sevgi/graphics/element.rb +23 -8
- data/lib/sevgi/graphics/mixtures/call.rb +9 -24
- data/lib/sevgi/graphics/mixtures/core.rb +19 -23
- data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
- data/lib/sevgi/graphics/mixtures/export.rb +8 -2
- data/lib/sevgi/graphics/mixtures/hatch.rb +5 -5
- data/lib/sevgi/graphics/mixtures/identify.rb +1 -1
- data/lib/sevgi/graphics/mixtures/inkscape.rb +23 -10
- data/lib/sevgi/graphics/mixtures/rdf.rb +1 -1
- data/lib/sevgi/graphics/mixtures/render.rb +11 -95
- data/lib/sevgi/graphics/mixtures/save.rb +4 -4
- data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
- data/lib/sevgi/graphics/mixtures/style.rb +36 -0
- data/lib/sevgi/graphics/mixtures/symbols.rb +2 -2
- data/lib/sevgi/graphics/mixtures/tile.rb +4 -4
- data/lib/sevgi/graphics/mixtures/transform.rb +22 -3
- data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +49 -0
- data/lib/sevgi/graphics/mixtures.rb +1 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +23 -19
- metadata +21 -7
- data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
- data/lib/sevgi/graphics/auxiliary/sizes.rb +0 -62
|
@@ -6,16 +6,18 @@ module Sevgi
|
|
|
6
6
|
#
|
|
7
7
|
# @!method self.valid?(name)
|
|
8
8
|
# Reports whether a candidate can dispatch as an SVG element name.
|
|
9
|
-
# With Standard loaded, the name must be known
|
|
9
|
+
# With Standard loaded, the name must be known. Standalone Graphics accepts any valid XML name.
|
|
10
10
|
# @param name [Object] candidate element name
|
|
11
11
|
# @return [Boolean]
|
|
12
12
|
#
|
|
13
13
|
# Dynamic SVG element methods accept text, content objects, and any number of attribute Hashes in one call. Hashes
|
|
14
|
-
# are applied from left to right
|
|
14
|
+
# are applied from left to right. Later values replace earlier values unless their names use the `+` update suffix.
|
|
15
|
+
# Ordinary `dup` and `clone` create independent subtrees, preserve IDs, and detach non-root copies from their parent.
|
|
16
|
+
# Document copies remain roots. Use `Duplicate` for DSL-specific ID remapping and attachment.
|
|
15
17
|
class Element
|
|
16
18
|
# Builds an element node.
|
|
17
19
|
# @param name [Symbol, String] SVG element name
|
|
18
|
-
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
20
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Every
|
|
19
21
|
# Hash is applied through {Sevgi::Graphics::Attributes} from left to right
|
|
20
22
|
# @param parent [Sevgi::Graphics::Element] parent element
|
|
21
23
|
# @yield evaluates the drawing DSL in the new element
|
|
@@ -178,9 +180,25 @@ module Sevgi
|
|
|
178
180
|
instance_exec(&block) if block
|
|
179
181
|
end
|
|
180
182
|
|
|
183
|
+
# Copies owned storage and reconnects child copies without attaching to the source tree.
|
|
184
|
+
# @param original [Sevgi::Graphics::Element] source node
|
|
185
|
+
# @return [void]
|
|
186
|
+
# @api private
|
|
187
|
+
def initialize_copy(original)
|
|
188
|
+
super
|
|
189
|
+
@parent = Element.root?(original) ? RootParent : DetachedParent
|
|
190
|
+
@attributes = original.attributes.dup
|
|
191
|
+
@contents = original.contents.map(&:dup)
|
|
192
|
+
@children = original.children.map do |child|
|
|
193
|
+
child.dup.tap { it.instance_variable_set(:@parent, self) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private :initialize_copy
|
|
198
|
+
|
|
181
199
|
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
182
200
|
# @param name [Symbol] missing method name
|
|
183
|
-
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
201
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Later
|
|
184
202
|
# Hashes replace or update attributes assigned by earlier Hashes
|
|
185
203
|
# @yield evaluates the drawing DSL in the dispatched child element
|
|
186
204
|
# @yieldreturn [Object] ignored block result
|
|
@@ -232,7 +250,7 @@ module Sevgi
|
|
|
232
250
|
|
|
233
251
|
# Parses element DSL arguments.
|
|
234
252
|
# @param name [Symbol] SVG element name
|
|
235
|
-
# @param args [Array<Object>] positional DSL arguments
|
|
253
|
+
# @param args [Array<Object>] positional DSL arguments. Hashes are imported from left to right
|
|
236
254
|
# @return [Hash] parsed :attributes and :contents
|
|
237
255
|
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
238
256
|
def parse(name, *args)
|
|
@@ -257,9 +275,6 @@ module Sevgi
|
|
|
257
275
|
|
|
258
276
|
private_constant :Dispatch
|
|
259
277
|
|
|
260
|
-
protected
|
|
261
|
-
|
|
262
|
-
attr_writer :attributes, :children, :contents, :parent
|
|
263
278
|
end
|
|
264
279
|
end
|
|
265
280
|
end
|
|
@@ -4,10 +4,10 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
# Callable drawing module support.
|
|
6
6
|
# Extend a plain Ruby module with this API to make its public instance methods callable drawing steps. Name the
|
|
7
|
-
# method `call` when the module has a single drawing step
|
|
7
|
+
# method `call` when the module has a single drawing step. Use descriptive method names for multiple steps.
|
|
8
8
|
# Base blocks add argument-independent shared SVG content once per invocation before the public drawing methods.
|
|
9
|
-
# Invocation does not change the configured module, so it
|
|
10
|
-
# duplicate or clone owns an independent configuration snapshot
|
|
9
|
+
# Invocation does not change the configured module, so it can be frozen after its drawing steps are defined. A
|
|
10
|
+
# duplicate or clone owns an independent configuration snapshot. Freezing a callable module prevents later base
|
|
11
11
|
# registration while leaving invocation available.
|
|
12
12
|
# @example Define and call a drawing module
|
|
13
13
|
# Widget = Module.new do
|
|
@@ -73,8 +73,9 @@ module Sevgi
|
|
|
73
73
|
|
|
74
74
|
# Registers argument-independent shared drawing steps. Every invocation runs inherited base blocks parent-first,
|
|
75
75
|
# then locally registered base blocks in registration order, before the module's public drawing methods. The block
|
|
76
|
-
# runs once
|
|
77
|
-
#
|
|
76
|
+
# runs once on the callable receiver, which delegates drawing words to the current SVG element. It receives no
|
|
77
|
+
# invocation arguments. Callable methods receive those arguments and the caller's block unchanged.
|
|
78
|
+
# @yield evaluates shared drawing steps on the callable receiver
|
|
78
79
|
# @yieldreturn [Object] ignored block result
|
|
79
80
|
# @return [nil]
|
|
80
81
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
@@ -133,7 +134,7 @@ module Sevgi
|
|
|
133
134
|
invoke(context, receiver, methods, ...)
|
|
134
135
|
end
|
|
135
136
|
|
|
136
|
-
# Returns the
|
|
137
|
+
# Returns the executable drawing methods for a callable module.
|
|
137
138
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
138
139
|
# @return [Array<UnboundMethod>]
|
|
139
140
|
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
@@ -173,26 +174,12 @@ module Sevgi
|
|
|
173
174
|
|
|
174
175
|
private
|
|
175
176
|
|
|
176
|
-
# Gives a
|
|
177
|
+
# Gives a copied callable module independent configuration containers.
|
|
177
178
|
# @param original [Module] source callable module
|
|
178
179
|
# @return [void]
|
|
179
180
|
# @api private
|
|
180
|
-
def
|
|
181
|
+
def initialize_copy(original)
|
|
181
182
|
super
|
|
182
|
-
copy_configuration(original)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Gives a cloned callable module independent configuration containers.
|
|
186
|
-
# @param original [Module] source callable module
|
|
187
|
-
# @param freeze [Boolean] whether Ruby preserves the source frozen state
|
|
188
|
-
# @return [void]
|
|
189
|
-
# @api private
|
|
190
|
-
def initialize_clone(original, freeze: true)
|
|
191
|
-
super
|
|
192
|
-
copy_configuration(original)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def copy_configuration(original)
|
|
196
183
|
@sevgi_bases = original.instance_variable_get(:@sevgi_bases).dup
|
|
197
184
|
@sevgi_callables = original.instance_variable_get(:@sevgi_callables).dup
|
|
198
185
|
@sevgi_configuration_owner = object_id
|
|
@@ -216,8 +203,6 @@ module Sevgi
|
|
|
216
203
|
own_configuration
|
|
217
204
|
@sevgi_callables << method
|
|
218
205
|
end
|
|
219
|
-
|
|
220
|
-
private :copy_configuration, :own_configuration
|
|
221
206
|
end
|
|
222
207
|
|
|
223
208
|
# Recursive callable drawing module support. Extend a module with this API when it and the modules defined beneath
|
|
@@ -93,24 +93,19 @@ module Sevgi
|
|
|
93
93
|
tap { Adoption.batch(elements, self, front: false) }
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Returns distinct CSS class names without changing this element.
|
|
97
|
+
# An absent class attribute returns an empty Array. Strings are split on whitespace and nested Arrays are flattened.
|
|
98
|
+
# @return [Array<String>] owned class-name snapshot in rendered order
|
|
99
|
+
# @see #Classify
|
|
100
|
+
def Classes = Attribute.classes(self[:class])
|
|
101
|
+
|
|
96
102
|
# Adds CSS classes without duplicating existing values.
|
|
97
|
-
# @param classes [Array<String, Symbol, Array>] class tokens
|
|
103
|
+
# @param classes [Array<String, Symbol, Array>] class tokens. Strings are split on whitespace
|
|
98
104
|
# @return [Sevgi::Graphics::Element] self
|
|
105
|
+
# @raise [Sevgi::ArgumentError] when a class value is invalid, cyclic, or cannot be stringified as XML
|
|
106
|
+
# @see #Classes
|
|
99
107
|
def Classify(*classes)
|
|
100
|
-
tap
|
|
101
|
-
tokens = proc do |value|
|
|
102
|
-
case value
|
|
103
|
-
when nil
|
|
104
|
-
[]
|
|
105
|
-
when ::Array
|
|
106
|
-
value.flat_map { tokens.call(it) }
|
|
107
|
-
else
|
|
108
|
-
value.to_s.split
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
self[:class] = [*tokens.call(self[:class]), *tokens.call(classes)].uniq
|
|
113
|
-
end
|
|
108
|
+
tap { self[:class] = Attribute.classes(self[:class], classes) }
|
|
114
109
|
end
|
|
115
110
|
|
|
116
111
|
# Assigns default attributes only when they are absent.
|
|
@@ -128,7 +123,7 @@ module Sevgi
|
|
|
128
123
|
|
|
129
124
|
# Builds a child element with an explicit tag name.
|
|
130
125
|
# @param tag [Symbol, String] SVG tag name
|
|
131
|
-
# @param contents [Array<Object>] text or content objects
|
|
126
|
+
# @param contents [Array<Object>] text or content objects. Non-content objects are stringified and XML-encoded
|
|
132
127
|
# @param attributes [Hash] SVG attributes
|
|
133
128
|
# @yield evaluates the drawing DSL in the new child element
|
|
134
129
|
# @yieldreturn [Object] ignored block result
|
|
@@ -176,7 +171,7 @@ module Sevgi
|
|
|
176
171
|
end
|
|
177
172
|
|
|
178
173
|
# Returns the topmost element in this tree.
|
|
179
|
-
# A detached subtree returns its detached topmost element
|
|
174
|
+
# A detached subtree returns its detached topmost element. Use {#Root?} to distinguish a document root.
|
|
180
175
|
# @return [Sevgi::Graphics::Element] document root or detached topmost element
|
|
181
176
|
def Root
|
|
182
177
|
element = self
|
|
@@ -283,7 +278,7 @@ module Sevgi
|
|
|
283
278
|
# Appends an element as a child.
|
|
284
279
|
# @param element [Sevgi::Graphics::Element] element to append
|
|
285
280
|
# @return [Sevgi::Graphics::Element] self
|
|
286
|
-
# @raise [Sevgi::ArgumentError] when the element has a different concrete class or
|
|
281
|
+
# @raise [Sevgi::ArgumentError] when the element has a different concrete class or creates a tree cycle
|
|
287
282
|
def <<(element)
|
|
288
283
|
Append(element)
|
|
289
284
|
end
|
|
@@ -296,7 +291,7 @@ module Sevgi
|
|
|
296
291
|
# @param parent [Sevgi::Graphics::Element] target parent
|
|
297
292
|
# @param front [Boolean] whether to prepend instead of append
|
|
298
293
|
# @return [void]
|
|
299
|
-
# @raise [Sevgi::ArgumentError] when an argument is incompatible, repeated, or
|
|
294
|
+
# @raise [Sevgi::ArgumentError] when an argument is incompatible, repeated, or creates a cycle
|
|
300
295
|
def self.batch(elements, parent, front:)
|
|
301
296
|
validate_batch(elements, parent)
|
|
302
297
|
|
|
@@ -307,11 +302,11 @@ module Sevgi
|
|
|
307
302
|
end
|
|
308
303
|
end
|
|
309
304
|
|
|
310
|
-
# Rejects target parents that
|
|
305
|
+
# Rejects target parents that create a cycle.
|
|
311
306
|
# @param element [Sevgi::Graphics::Element] element being moved
|
|
312
307
|
# @param parent [Sevgi::Graphics::Element, Object] target parent
|
|
313
308
|
# @return [void]
|
|
314
|
-
# @raise [Sevgi::ArgumentError] when the parent is incompatible or
|
|
309
|
+
# @raise [Sevgi::ArgumentError] when the parent is incompatible or creates a cycle
|
|
315
310
|
def self.validate(element, parent)
|
|
316
311
|
unless element.instance_of?(parent.class)
|
|
317
312
|
ArgumentError.("Element type does not match the new parent type: #{element.class}")
|
|
@@ -347,8 +342,9 @@ module Sevgi
|
|
|
347
342
|
# @param index [Integer] requested insertion index
|
|
348
343
|
# @return [Integer] normalized insertion index
|
|
349
344
|
def self.index_for(element, parent, index)
|
|
350
|
-
|
|
351
|
-
|
|
345
|
+
children = Element.send(:tree_children, parent)
|
|
346
|
+
same_parent = element.parent.equal?(parent) && children.include?(element)
|
|
347
|
+
index(index, children.size - (same_parent ? 1 : 0))
|
|
352
348
|
end
|
|
353
349
|
|
|
354
350
|
def self.validate_batch(elements, parent)
|
|
@@ -10,8 +10,9 @@ module Sevgi
|
|
|
10
10
|
# moved to non-rendering `-id` metadata before the optional block runs, allowing the block to derive replacement
|
|
11
11
|
# ids without rendering duplicates. A pre-existing `-id` takes precedence over the visible id.
|
|
12
12
|
# Translation and parent channels are validated before the subtree is copied or the customization block runs.
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
13
|
+
# The copied subtree has no parent during customization and is attached to the target parent afterward.
|
|
14
|
+
# @param dx [Numeric, nil] finite x translation. Nil omits the axis
|
|
15
|
+
# @param dy [Numeric, nil] finite y translation. Nil omits the axis
|
|
15
16
|
# @param parent [Sevgi::Graphics::Element, nil] explicit parent, or the source parent when nil
|
|
16
17
|
# @yield [element] optional customization hook for each copied element
|
|
17
18
|
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
@@ -27,7 +28,7 @@ module Sevgi
|
|
|
27
28
|
# end
|
|
28
29
|
def Duplicate(dx: nil, dy: nil, parent: nil, &block)
|
|
29
30
|
dx, dy, target = Subtree.channels(self, dx, dy, parent)
|
|
30
|
-
duplicated =
|
|
31
|
+
duplicated = dup
|
|
31
32
|
Subtree.prepare(duplicated, &block)
|
|
32
33
|
Subtree.translate(duplicated, dx, dy)
|
|
33
34
|
Subtree.attach(duplicated, target)
|
|
@@ -80,19 +81,6 @@ module Sevgi
|
|
|
80
81
|
[dx, dy, target]
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
# Builds an independent copy of an element subtree.
|
|
84
|
-
# @param element [Sevgi::Graphics::Element] source subtree root
|
|
85
|
-
# @param parent [Sevgi::Graphics::Element, Object] parent for the copied root
|
|
86
|
-
# @return [Sevgi::Graphics::Element] copied subtree root
|
|
87
|
-
def self.copy(element, parent = Element.send(:tree_parent, element))
|
|
88
|
-
element.dup.tap do |duplicated|
|
|
89
|
-
duplicated.send(:parent=, parent)
|
|
90
|
-
duplicated.send(:attributes=, element.attributes.dup)
|
|
91
|
-
duplicated.send(:contents=, element.contents.map(&:dup))
|
|
92
|
-
duplicated.send(:children=, element.children.map { |child| copy(child, duplicated) })
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
84
|
# Removes copied public ids and applies an optional customization hook.
|
|
97
85
|
# @api private
|
|
98
86
|
def self.prepare(element, &block)
|
|
@@ -4,11 +4,13 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for native SVG export formats.
|
|
7
|
+
# Export CSS is a late adjustment after document checks. Its supported XML endings and cascade limitations are
|
|
8
|
+
# documented in {Sevgi::Sundries::Export}. The source callback receives SVG after CSS insertion.
|
|
7
9
|
module Export
|
|
8
10
|
# Validates the closed export option channel before optional components load or rendering starts.
|
|
9
11
|
# @api private
|
|
10
12
|
module Options
|
|
11
|
-
KEYS = %i[css default dpi height width].freeze
|
|
13
|
+
KEYS = %i[css default dpi fallback height style width].freeze
|
|
12
14
|
private_constant :KEYS
|
|
13
15
|
|
|
14
16
|
# Returns validated export options.
|
|
@@ -34,6 +36,8 @@ module Sevgi
|
|
|
34
36
|
# @option kwargs [Numeric, nil] :height finite positive target height in CSS pixels
|
|
35
37
|
# @option kwargs [Numeric] :dpi (96.0) finite positive CSS pixel density
|
|
36
38
|
# @option kwargs [String, nil] :css CSS inserted before rendering
|
|
39
|
+
# @option kwargs [Hash, nil] :style (document Style) producer declaration
|
|
40
|
+
# @option kwargs [Boolean] :fallback (true) warn and use ordinary PDF after preparation failure
|
|
37
41
|
# @yield [svg] transforms SVG source before rendering
|
|
38
42
|
# @yieldparam svg [String] rendered SVG source
|
|
39
43
|
# @yieldreturn [String] transformed SVG source
|
|
@@ -44,6 +48,7 @@ module Sevgi
|
|
|
44
48
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
45
49
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
46
50
|
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
51
|
+
# @raise [Sevgi::Sundries::Mainz::Error] when required preparation cannot complete
|
|
47
52
|
# @raise [SystemCallError] when the output directory or file cannot be created or written
|
|
48
53
|
def PDF(path = nil, **kwargs, &block)
|
|
49
54
|
kwargs = Options.(kwargs)
|
|
@@ -56,6 +61,7 @@ module Sevgi
|
|
|
56
61
|
MissingComponentError.("sevgi/sundries")
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
kwargs[:style] = self.Style unless kwargs.key?(:style) || self.Style.nil?
|
|
59
65
|
Export(path, **kwargs, format: :pdf, &block)
|
|
60
66
|
end
|
|
61
67
|
|
|
@@ -102,7 +108,7 @@ module Sevgi
|
|
|
102
108
|
default = F.subext(extension, caller_locations(2..2).first.path)
|
|
103
109
|
end
|
|
104
110
|
|
|
105
|
-
path =
|
|
111
|
+
path = F.output_path(path, default:, context: "Export")
|
|
106
112
|
|
|
107
113
|
Sundries::Export.(call, path, **kwargs, &block)
|
|
108
114
|
end
|
|
@@ -6,10 +6,10 @@ module Sevgi
|
|
|
6
6
|
# DSL helpers for drawing geometry values and geometry-derived hatch lines.
|
|
7
7
|
#
|
|
8
8
|
# `Draw` delegates to each geometry object's drawing protocol. `Hatch`
|
|
9
|
-
# first sweeps interior spans through a closed
|
|
10
|
-
# them. Its `angle` describes line direction
|
|
9
|
+
# first sweeps interior spans through a closed geometry element, then draws
|
|
10
|
+
# them. Its `angle` describes line direction. `step` is perpendicular
|
|
11
11
|
# spacing. The default initial line passes through `element.position`.
|
|
12
|
-
# The built-in `:inkscape` document profile includes this mixture
|
|
12
|
+
# The built-in `:inkscape` document profile includes this mixture.
|
|
13
13
|
# `:minimal`, `:default`, and `:html` do not.
|
|
14
14
|
#
|
|
15
15
|
# Hatch materializes separate finite SVG path elements. When only a repeated visual fill matters, use an SVG
|
|
@@ -52,7 +52,7 @@ module Sevgi
|
|
|
52
52
|
# Sevgi::Graphics.SVG(:inkscape) do
|
|
53
53
|
# Hatch region, initial: [2, 8], angle: 0, step: 3
|
|
54
54
|
# end
|
|
55
|
-
# @param element [Sevgi::Geometry::Element
|
|
55
|
+
# @param element [Sevgi::Geometry::Element] geometry element to sweep
|
|
56
56
|
# @param angle [Numeric] hatch angle in degrees
|
|
57
57
|
# @param step [Numeric] distance between hatch lines
|
|
58
58
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>, nil] initial sweep point, or nil for element.position
|
|
@@ -72,7 +72,7 @@ module Sevgi
|
|
|
72
72
|
MissingComponentError.("sevgi/geometry")
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
initial = element.position if initial.nil? && element.is_a?(Geometry::Element
|
|
75
|
+
initial = element.position if initial.nil? && element.is_a?(Geometry::Element)
|
|
76
76
|
Draw(Geometry::Operation.sweep!(element, initial:, angle:, step:), **kwargs)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
@@ -6,7 +6,7 @@ module Sevgi
|
|
|
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
|
|
9
|
+
# `"false"` and the empty String. Keys and containers are owned by the index. Values retain references to the
|
|
10
10
|
# elements present when the snapshot is built. Later tree changes require a new index.
|
|
11
11
|
class Identifiers
|
|
12
12
|
# @return [Sevgi::Graphics::Element] indexed root element
|
|
@@ -48,7 +48,7 @@ module Sevgi
|
|
|
48
48
|
# Sevgi::Graphics.SVG(:inkscape) { Group drawing, attributes: {id: "drawing"} }
|
|
49
49
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
50
50
|
# @param args [Array<Object>] callable arguments
|
|
51
|
-
# @param attributes [Hash] group attributes
|
|
51
|
+
# @param attributes [Hash] group attributes. String and Symbol names are normalized and must not collide
|
|
52
52
|
# @param kwargs [Hash] callable keyword arguments
|
|
53
53
|
# @yield forwards customization to the callable module
|
|
54
54
|
# @yieldreturn [Object] callable customization result
|
|
@@ -65,7 +65,7 @@ module Sevgi
|
|
|
65
65
|
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
66
66
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
67
67
|
# @param args [Array<Object>] callable arguments
|
|
68
|
-
# @param attributes [Hash] layer attributes
|
|
68
|
+
# @param attributes [Hash] layer attributes. String and Symbol names are normalized and must not collide
|
|
69
69
|
# @param kwargs [Hash] callable keyword arguments
|
|
70
70
|
# @yield forwards customization to the callable module
|
|
71
71
|
# @yieldreturn [Object] callable customization result
|
|
@@ -82,7 +82,7 @@ module Sevgi
|
|
|
82
82
|
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
83
83
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
84
84
|
# @param args [Array<Object>] callable arguments
|
|
85
|
-
# @param attributes [Hash] layer attributes
|
|
85
|
+
# @param attributes [Hash] layer attributes. String and Symbol names are normalized and must not collide
|
|
86
86
|
# @param kwargs [Hash] callable keyword arguments
|
|
87
87
|
# @yield forwards customization to the callable module
|
|
88
88
|
# @yieldreturn [Object] callable customization result
|
|
@@ -163,7 +163,7 @@ module Sevgi
|
|
|
163
163
|
def channels(namedview, page)
|
|
164
164
|
ArgumentError.("Namedview attributes must be a Hash") unless namedview.is_a?(::Hash)
|
|
165
165
|
ArgumentError.("Page attributes must be a Hash") unless page.is_a?(::Hash)
|
|
166
|
-
[Attribute.defaults(namedview, id: "namedview"),
|
|
166
|
+
[Attribute.defaults(namedview, id: "namedview"), page_attributes(page)]
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Normalizes one explicit or generated page id.
|
|
@@ -181,7 +181,7 @@ module Sevgi
|
|
|
181
181
|
# @api private
|
|
182
182
|
def normalize(attributes, defaults, index)
|
|
183
183
|
ArgumentError.("Page #{index + 1} must be a Hash") unless attributes.is_a?(::Hash)
|
|
184
|
-
attributes = defaults.merge(
|
|
184
|
+
attributes = defaults.merge(page_attributes(attributes))
|
|
185
185
|
%i[x y].each { number(attributes, it, index) }
|
|
186
186
|
%i[width height].each do |field|
|
|
187
187
|
number(attributes, field, index, positive: true)
|
|
@@ -190,6 +190,19 @@ module Sevgi
|
|
|
190
190
|
identify(attributes, index)
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# Coerces numeric page fields before attribute snapshots stringify mutable numeric subclasses.
|
|
194
|
+
def page_attributes(attributes)
|
|
195
|
+
Attribute.normalize(
|
|
196
|
+
attributes.to_h do |key, value|
|
|
197
|
+
if %w[x y width height].include?(key.to_s) && value.is_a?(::Numeric)
|
|
198
|
+
value = Scalar.number(value, context: "page", field: key)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
[key, value]
|
|
202
|
+
end
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
193
206
|
# Validates and normalizes page-grid arguments.
|
|
194
207
|
# @return [Array<(Integer, Float)>] normalized width, height, and gap
|
|
195
208
|
# @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
|
|
@@ -239,9 +252,9 @@ module Sevgi
|
|
|
239
252
|
# page: {class: "print"}
|
|
240
253
|
# )
|
|
241
254
|
# end
|
|
242
|
-
# @param pages [Array<Hash>] page attribute hashes
|
|
243
|
-
# @param namedview [Hash] namedview attributes
|
|
244
|
-
# @param page [Hash] page defaults
|
|
255
|
+
# @param pages [Array<Hash>] page attribute hashes. Numeric page fields are normalized to SVG numbers
|
|
256
|
+
# @param namedview [Hash] namedview attributes. String and Symbol names are normalized and must not collide
|
|
257
|
+
# @param page [Hash] page defaults. Normalized page attributes override these defaults by name
|
|
245
258
|
# @yield [page] customizes each generated page element
|
|
246
259
|
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
247
260
|
# @yieldreturn [Object] ignored customization result
|
|
@@ -262,8 +275,8 @@ module Sevgi
|
|
|
262
275
|
# @param width [Numeric] finite positive page width, normalized to an SVG number
|
|
263
276
|
# @param height [Numeric] finite positive page height, normalized to an SVG number
|
|
264
277
|
# @param gap [Numeric] finite non-negative gap, normalized to an SVG number
|
|
265
|
-
# @param namedview [Hash] namedview attributes
|
|
266
|
-
# @param page [Hash] page defaults
|
|
278
|
+
# @param namedview [Hash] namedview attributes. String and Symbol names are normalized and must not collide
|
|
279
|
+
# @param page [Hash] page defaults. Normalized page attributes override these defaults by name
|
|
267
280
|
# @yield [page] customizes each generated page element
|
|
268
281
|
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
269
282
|
# @yieldreturn [Object] ignored customization result
|
|
@@ -114,7 +114,7 @@ module Sevgi
|
|
|
114
114
|
def License_LAL(**kwargs, &block) = License(**kwargs, license: "https://artlibre.org/licence/lal/en/", &block)
|
|
115
115
|
|
|
116
116
|
# Builds an RDF root element.
|
|
117
|
-
# @param kwargs [Hash] options
|
|
117
|
+
# @param kwargs [Hash] options. RDF currently accepts none
|
|
118
118
|
# @yield evaluates the RDF drawing DSL
|
|
119
119
|
# @yieldreturn [Object] ignored block result
|
|
120
120
|
# @return [Sevgi::Graphics::Element] RDF element
|
|
@@ -13,68 +13,6 @@ module Sevgi
|
|
|
13
13
|
STYLES = %i[hybrid inline block].freeze
|
|
14
14
|
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
15
15
|
|
|
16
|
-
# Attribute rendering strategies.
|
|
17
|
-
# @api private
|
|
18
|
-
module Attributes
|
|
19
|
-
# Block-style attribute renderer.
|
|
20
|
-
# @api private
|
|
21
|
-
module Block
|
|
22
|
-
# Renders attributes in block form.
|
|
23
|
-
# @param element [Sevgi::Graphics::Element] rendered element
|
|
24
|
-
# @param depth [Integer] element depth
|
|
25
|
-
# @return [void]
|
|
26
|
-
def attributes(element, depth)
|
|
27
|
-
attributes_block(element, depth, element.attributes.send(:xml_lines))
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Hybrid attribute renderer.
|
|
32
|
-
# @api private
|
|
33
|
-
module Hybrid
|
|
34
|
-
# Renders attributes inline or in block form according to line length.
|
|
35
|
-
# @param element [Sevgi::Graphics::Element] rendered element
|
|
36
|
-
# @param depth [Integer] element depth
|
|
37
|
-
# @return [void]
|
|
38
|
-
def attributes(element, depth)
|
|
39
|
-
if attributes_as_block?(lines = element.attributes.send(:xml_lines), depth)
|
|
40
|
-
attributes_block(element, depth, lines)
|
|
41
|
-
else
|
|
42
|
-
attributes_inline(element, depth, lines)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Reports whether attributes should be rendered in block form.
|
|
47
|
-
# @param lines [Array<String>] rendered attribute lines
|
|
48
|
-
# @param depth [Integer] element depth
|
|
49
|
-
# @return [Boolean]
|
|
50
|
-
def attributes_as_block?(lines, depth)
|
|
51
|
-
linelength(lines, depth) > options[:linelength]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Returns the effective inline line length.
|
|
55
|
-
# @param lines [Array<String>] rendered attribute lines
|
|
56
|
-
# @param depth [Integer] element depth
|
|
57
|
-
# @return [Integer]
|
|
58
|
-
def linelength(lines, depth)
|
|
59
|
-
indent(depth).length + lines.sum(&:length)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Inline attribute renderer.
|
|
64
|
-
# @api private
|
|
65
|
-
module Inline
|
|
66
|
-
# Renders attributes inline.
|
|
67
|
-
# @param element [Sevgi::Graphics::Element] rendered element
|
|
68
|
-
# @param depth [Integer] element depth
|
|
69
|
-
# @return [void]
|
|
70
|
-
def attributes(element, depth)
|
|
71
|
-
attributes_inline(element, depth, element.attributes.send(:xml_lines))
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private_constant :Attributes
|
|
77
|
-
|
|
78
16
|
ELEMENTS_WITH_INLINE_CONTENT = %i[title].freeze
|
|
79
17
|
ELEMENTS_WITH_BLOCK_CONTENT = %i[style].freeze
|
|
80
18
|
SEPARATOR = "\n"
|
|
@@ -102,17 +40,11 @@ module Sevgi
|
|
|
102
40
|
# Inline mark with start, stop, and depth.
|
|
103
41
|
Mark = Struct.new(:start, :stop, :depth)
|
|
104
42
|
|
|
105
|
-
# Creates an inline splice tracker.
|
|
106
|
-
# @return [void]
|
|
107
43
|
def initialize
|
|
108
44
|
@marks = []
|
|
109
45
|
@stack = []
|
|
110
46
|
end
|
|
111
47
|
|
|
112
|
-
# Starts an inline splice range.
|
|
113
|
-
# @param index [Integer] output index
|
|
114
|
-
# @param depth [Integer] element depth
|
|
115
|
-
# @return [Sevgi::Graphics::Mixtures::Render::Renderer::Inlines::Mark] opened mark
|
|
116
48
|
def start(index, depth)
|
|
117
49
|
Mark.new(start: index, depth:).tap do |mark|
|
|
118
50
|
@marks << mark
|
|
@@ -120,10 +52,6 @@ module Sevgi
|
|
|
120
52
|
end
|
|
121
53
|
end
|
|
122
54
|
|
|
123
|
-
# Ends the innermost inline splice range.
|
|
124
|
-
# @param index [Integer] output index
|
|
125
|
-
# @return [Integer]
|
|
126
|
-
# @raise [Sevgi::PanicError] when no inline range is open
|
|
127
55
|
def stop(index)
|
|
128
56
|
mark = @stack.pop
|
|
129
57
|
PanicError.("Inline content range was not opened") unless mark
|
|
@@ -131,12 +59,6 @@ module Sevgi
|
|
|
131
59
|
mark.stop = index
|
|
132
60
|
end
|
|
133
61
|
|
|
134
|
-
# Joins marked output ranges into inline content.
|
|
135
|
-
# @param output [Array<Array<String>, nil>] renderer output buffer
|
|
136
|
-
# @param indent [String] indentation unit
|
|
137
|
-
# @param separator [String] line separator
|
|
138
|
-
# @return [Array<Array<String>, nil>, nil]
|
|
139
|
-
# @raise [Sevgi::PanicError] when an inline range was not closed
|
|
140
62
|
def join(output, indent:, separator:)
|
|
141
63
|
return if @marks.empty?
|
|
142
64
|
|
|
@@ -175,8 +97,7 @@ module Sevgi
|
|
|
175
97
|
@options = self.class.send(:validate, **)
|
|
176
98
|
@output = []
|
|
177
99
|
@inlines = Inlines.new
|
|
178
|
-
|
|
179
|
-
build
|
|
100
|
+
unclosed
|
|
180
101
|
end
|
|
181
102
|
|
|
182
103
|
# @overload call(root, **options)
|
|
@@ -246,6 +167,14 @@ module Sevgi
|
|
|
246
167
|
|
|
247
168
|
attr_reader :inlines
|
|
248
169
|
|
|
170
|
+
def attributes(element, depth)
|
|
171
|
+
lines = element.attributes.send(:xml_lines)
|
|
172
|
+
block = options[:style] == :block ||
|
|
173
|
+
(options[:style] == :hybrid && indent(depth).length + lines.sum(&:length) > options[:linelength])
|
|
174
|
+
|
|
175
|
+
block ? attributes_block(element, depth, lines) : attributes_inline(element, depth, lines)
|
|
176
|
+
end
|
|
177
|
+
|
|
249
178
|
def attributes_block(element, depth, lines)
|
|
250
179
|
return attributes_inline(element, depth, lines) if lines.empty?
|
|
251
180
|
|
|
@@ -265,19 +194,6 @@ module Sevgi
|
|
|
265
194
|
end
|
|
266
195
|
end
|
|
267
196
|
|
|
268
|
-
def build
|
|
269
|
-
case options[:style]
|
|
270
|
-
when :hybrid
|
|
271
|
-
extend(Attributes::Hybrid)
|
|
272
|
-
when :inline
|
|
273
|
-
extend(Attributes::Inline)
|
|
274
|
-
when :block
|
|
275
|
-
extend(Attributes::Block)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
unclosed
|
|
279
|
-
end
|
|
280
|
-
|
|
281
197
|
def childless?(element)
|
|
282
198
|
element.children.empty? && element.contents.empty?
|
|
283
199
|
end
|
|
@@ -361,9 +277,9 @@ module Sevgi
|
|
|
361
277
|
|
|
362
278
|
# @overload Render(**options)
|
|
363
279
|
# Renders this element as SVG source.
|
|
364
|
-
# Elements with inline text content
|
|
280
|
+
# Elements with inline text content can also contain inline children such as `tspan`. The renderer keeps those
|
|
365
281
|
# descendants in the same text line. Whitespace inside content objects is preserved as given, and encoded
|
|
366
|
-
# content is XML-escaped unless a verbatim content object is used. SVG `style` elements use block content
|
|
282
|
+
# content is XML-escaped unless a verbatim content object is used. SVG `style` elements use block content.
|
|
367
283
|
# same-named elements under a foreign default namespace retain ordinary inline text formatting.
|
|
368
284
|
# @example Keep every attribute on the element's opening line
|
|
369
285
|
# SVG(:minimal) { rect id: "card", width: 80, height: 40 }.Render(style: :inline)
|