sevgi-graphics 0.73.2 → 0.94.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 +4 -0
- data/LICENSE +5 -0
- data/README.md +37 -0
- data/lib/sevgi/graphics/attribute.rb +82 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +153 -0
- data/lib/sevgi/graphics/auxiliary/content.rb +157 -0
- data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
- data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
- data/lib/sevgi/graphics/auxiliary.rb +7 -0
- data/lib/sevgi/graphics/document/base.rb +6 -0
- data/lib/sevgi/graphics/document/default.rb +1 -0
- data/lib/sevgi/graphics/document/html.rb +1 -0
- data/lib/sevgi/graphics/document/inkscape.rb +1 -0
- data/lib/sevgi/graphics/document/minimal.rb +1 -0
- data/lib/sevgi/graphics/document.rb +205 -13
- data/lib/sevgi/graphics/element.rb +69 -9
- data/lib/sevgi/graphics/mixtures/call.rb +62 -4
- data/lib/sevgi/graphics/mixtures/core.rb +146 -25
- data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -3
- data/lib/sevgi/graphics/mixtures/export.rb +31 -6
- data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
- data/lib/sevgi/graphics/mixtures/identify.rb +26 -2
- data/lib/sevgi/graphics/mixtures/include.rb +40 -4
- data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
- data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
- data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
- data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
- data/lib/sevgi/graphics/mixtures/render.rb +136 -14
- data/lib/sevgi/graphics/mixtures/save.rb +21 -3
- data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
- data/lib/sevgi/graphics/mixtures/tile.rb +52 -9
- data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
- data/lib/sevgi/graphics/mixtures/underscore.rb +21 -1
- data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
- data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
- data/lib/sevgi/graphics/mixtures.rb +35 -3
- data/lib/sevgi/graphics/version.rb +2 -1
- data/lib/sevgi/graphics.rb +71 -5
- metadata +12 -9
- data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
- data/lib/sevgi/graphics/auxilary/content.rb +0 -81
- data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
- data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
- data/lib/sevgi/graphics/auxilary.rb +0 -7
|
@@ -2,19 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
|
+
# Callable drawing module support.
|
|
6
|
+
# Extend a plain Ruby module with this API to make its public instance methods callable drawing steps.
|
|
7
|
+
# @example Define and call a drawing module
|
|
8
|
+
# Widget = Module.new do
|
|
9
|
+
# extend Sevgi::Graphics::Module
|
|
10
|
+
#
|
|
11
|
+
# def item(id)
|
|
12
|
+
# rect(id:)
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# SVG { Call(Widget, "box") }
|
|
5
17
|
module Module
|
|
18
|
+
# Tracks newly defined methods as callable drawing candidates.
|
|
19
|
+
# Invocation runs unique methods that are still public, preserving tracked definition order.
|
|
20
|
+
# @param method [Symbol] method name Ruby reports as added
|
|
21
|
+
# @return [Array<Symbol>, nil]
|
|
6
22
|
def method_added(method)
|
|
7
23
|
super
|
|
8
24
|
|
|
9
25
|
_callables << method if public_method_defined?(method)
|
|
10
26
|
end
|
|
11
27
|
|
|
28
|
+
# Class-level DSL for callable drawing modules.
|
|
29
|
+
# @api private
|
|
12
30
|
module DSL
|
|
13
|
-
|
|
31
|
+
# Registers a before or after hook.
|
|
32
|
+
# @param after [Boolean] true to register an after hook
|
|
33
|
+
# @return [Array<Proc>] hook list
|
|
34
|
+
def call(after = false, &block) = ((after ? _afters : _befores) << block)
|
|
14
35
|
end
|
|
15
36
|
|
|
16
37
|
private_constant :DSL
|
|
17
38
|
|
|
39
|
+
# Initializes callable module state.
|
|
40
|
+
# @param base [Module] extended module
|
|
41
|
+
# @return [void]
|
|
18
42
|
def self.extended(base)
|
|
19
43
|
base.instance_exec do
|
|
20
44
|
@_callables = []
|
|
@@ -29,6 +53,14 @@ module Sevgi
|
|
|
29
53
|
end
|
|
30
54
|
end
|
|
31
55
|
|
|
56
|
+
# @overload call(mod, receiver, *args, **kwargs)
|
|
57
|
+
# Runs module hooks and callables against a receiver.
|
|
58
|
+
# @param mod [Module] callable module
|
|
59
|
+
# @param receiver [Sevgi::Graphics::Element] receiver element
|
|
60
|
+
# @param args [Array<Object>] callable arguments
|
|
61
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
62
|
+
# @return [Object, nil] last callable return value
|
|
63
|
+
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
32
64
|
def self.call(mod, receiver, ...)
|
|
33
65
|
mod._befores.each { receiver.Within(receiver, &it) } if mod.respond_to?(:_befores) && mod._befores
|
|
34
66
|
# return last callable return value
|
|
@@ -37,15 +69,41 @@ module Sevgi
|
|
|
37
69
|
end
|
|
38
70
|
end
|
|
39
71
|
|
|
72
|
+
# Returns the methods that should be executed for a callable module.
|
|
73
|
+
# @param mod [Module] callable module
|
|
74
|
+
# @return [Array<UnboundMethod>]
|
|
75
|
+
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
40
76
|
def self.callables(mod)
|
|
41
|
-
|
|
77
|
+
ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
|
|
42
78
|
|
|
43
|
-
(mod
|
|
79
|
+
callable_names(mod).uniq.filter_map do |name|
|
|
80
|
+
mod.instance_method(name) if mod.public_method_defined?(name)
|
|
81
|
+
end
|
|
44
82
|
end
|
|
83
|
+
|
|
84
|
+
def self.callable_names(mod)
|
|
85
|
+
tracked = mod.ancestors.reverse_each.filter_map do |ancestor|
|
|
86
|
+
ancestor._callables if ancestor.respond_to?(:_callables)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return tracked.flatten unless tracked.empty?
|
|
90
|
+
|
|
91
|
+
mod.public_instance_methods
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private_class_method :callable_names
|
|
45
95
|
end
|
|
46
96
|
|
|
47
97
|
module Mixtures
|
|
98
|
+
# DSL helpers for invoking callable drawing modules.
|
|
48
99
|
module Call
|
|
100
|
+
# @overload Call(mod, *args, **kwargs)
|
|
101
|
+
# Runs a callable drawing module in the current element context.
|
|
102
|
+
# @param mod [Module] callable module
|
|
103
|
+
# @param args [Array<Object>] callable arguments
|
|
104
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
105
|
+
# @return [Object, nil] last callable return value
|
|
106
|
+
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
49
107
|
def Call(mod, ...)
|
|
50
108
|
Graphics::Module.call(mod, self, ...)
|
|
51
109
|
end
|
|
@@ -54,7 +112,7 @@ module Sevgi
|
|
|
54
112
|
|
|
55
113
|
# rubocop:disable Metrics/MethodLength
|
|
56
114
|
def CallWithin(mod, container, element, *args, **kwargs, &block)
|
|
57
|
-
|
|
115
|
+
ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
|
|
58
116
|
|
|
59
117
|
kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
|
|
60
118
|
|
|
@@ -5,11 +5,54 @@ require "forwardable"
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Graphics
|
|
7
7
|
module Mixtures
|
|
8
|
+
# Traversal stop token returned by {Core#Stay}.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] value
|
|
11
|
+
# @return [Object] value returned from traversal
|
|
12
|
+
Stop = Data.define(:value)
|
|
13
|
+
|
|
14
|
+
# Internal traversal engine.
|
|
15
|
+
# @api private
|
|
16
|
+
module Traversal
|
|
17
|
+
# Runs a depth-first traversal.
|
|
18
|
+
# @param element [Sevgi::Graphics::Element] starting element
|
|
19
|
+
# @param depth [Integer] starting depth
|
|
20
|
+
# @param leave [Proc, nil] optional leave callback
|
|
21
|
+
# @return [Sevgi::Graphics::Element, Object]
|
|
22
|
+
def self.call(element, depth, leave, &block)
|
|
23
|
+
catch(:traversal) do
|
|
24
|
+
visit(element, depth, leave, &block)
|
|
25
|
+
element
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.stop(value)
|
|
30
|
+
throw(:traversal, value.value) if value.is_a?(Stop)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.visit(element, depth, leave, &block)
|
|
34
|
+
stop(block.call(element, depth))
|
|
35
|
+
element.children.each { |child| visit(child, depth + 1, leave, &block) }
|
|
36
|
+
stop(leave.call(element, depth)) if leave
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private_class_method :stop, :visit
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_constant :Traversal
|
|
43
|
+
|
|
44
|
+
# Core SVG tree and attribute DSL helpers.
|
|
8
45
|
module Core
|
|
9
46
|
extend Forwardable
|
|
10
47
|
def_delegators :@attributes, :[], :[]=, :has?
|
|
11
48
|
def_delegators :@children, :first, :last, :at
|
|
12
49
|
|
|
50
|
+
# Moves this element under a new parent.
|
|
51
|
+
# @param new_parent [Sevgi::Graphics::Element, nil] target parent or current parent
|
|
52
|
+
# @param index [Integer] insertion index
|
|
53
|
+
# @return [Sevgi::Graphics::Element] self
|
|
54
|
+
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
55
|
+
# @raise [Sevgi::ArgumentError] when the target parent is this element or one of its descendants
|
|
13
56
|
def Adopt(new_parent = nil, index: -1)
|
|
14
57
|
tap do
|
|
15
58
|
if new_parent
|
|
@@ -20,101 +63,146 @@ module Sevgi
|
|
|
20
63
|
new_parent = parent
|
|
21
64
|
end
|
|
22
65
|
|
|
66
|
+
Adoption.validate(self, new_parent)
|
|
67
|
+
|
|
23
68
|
self.Orphan()
|
|
24
69
|
(@parent = new_parent).children.insert(index, self)
|
|
25
70
|
end
|
|
26
71
|
end
|
|
27
72
|
|
|
73
|
+
# @overload AdoptFirst(new_parent = nil)
|
|
74
|
+
# Moves this element to the beginning of a parent.
|
|
75
|
+
# @param new_parent [Sevgi::Graphics::Element, nil] target parent or current parent
|
|
76
|
+
# @return [Sevgi::Graphics::Element] self
|
|
77
|
+
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
78
|
+
# @raise [Sevgi::ArgumentError] when the target parent is this element or one of its descendants
|
|
28
79
|
def AdoptFirst(*)
|
|
29
80
|
Adopt(*, index: 0)
|
|
30
81
|
end
|
|
31
82
|
|
|
83
|
+
# Appends existing elements as children.
|
|
84
|
+
# @param elements [Array<Sevgi::Graphics::Element>] elements to append
|
|
85
|
+
# @return [Sevgi::Graphics::Element] self
|
|
32
86
|
def Append(*elements)
|
|
33
87
|
tap { elements.each { it.Adopt(self) } }
|
|
34
88
|
end
|
|
35
89
|
|
|
90
|
+
# Adds CSS classes without duplicating existing values.
|
|
91
|
+
# @param classes [Array<String, Symbol, Array>] class tokens; strings are split on whitespace
|
|
92
|
+
# @return [Sevgi::Graphics::Element] self
|
|
36
93
|
def Classify(*classes)
|
|
37
94
|
tap do
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
95
|
+
tokens = proc do |value|
|
|
96
|
+
case value
|
|
97
|
+
when nil
|
|
98
|
+
[]
|
|
99
|
+
when ::Array
|
|
100
|
+
value.flat_map { tokens.call(it) }
|
|
101
|
+
else
|
|
102
|
+
value.to_s.split
|
|
103
|
+
end
|
|
41
104
|
end
|
|
42
105
|
|
|
43
|
-
|
|
44
|
-
when ::Array
|
|
45
|
-
self[:class]
|
|
46
|
-
when ::String
|
|
47
|
-
self[:class].split
|
|
48
|
-
end => klasses
|
|
49
|
-
|
|
50
|
-
classes.each { klasses << it unless klasses.include?(it) }
|
|
106
|
+
self[:class] = [*tokens.call(self[:class]), *tokens.call(classes)].uniq
|
|
51
107
|
end
|
|
52
108
|
end
|
|
53
109
|
|
|
110
|
+
# Assigns default attributes only when they are absent.
|
|
111
|
+
# @param attributes [Hash] default attributes
|
|
112
|
+
# @return [Sevgi::Graphics::Element] self
|
|
54
113
|
def Defaults(**attributes)
|
|
55
114
|
tap do
|
|
56
115
|
attributes.each do |key, value|
|
|
57
|
-
next if
|
|
116
|
+
next if has?(key)
|
|
58
117
|
|
|
59
118
|
self[key] = value
|
|
60
119
|
end
|
|
61
120
|
end
|
|
62
121
|
end
|
|
63
122
|
|
|
123
|
+
# Builds a child element with an explicit tag name.
|
|
124
|
+
# @param tag [Symbol, String] SVG tag name
|
|
125
|
+
# @param contents [Array<Object>] text or content objects; non-content objects are stringified and XML-encoded
|
|
126
|
+
# @param attributes [Hash] SVG attributes
|
|
127
|
+
# @return [Sevgi::Graphics::Element] new child element
|
|
64
128
|
def Element(tag, *contents, **attributes, &block)
|
|
65
129
|
self.class.send(:new, tag.to_sym, contents: Content.contents(*contents), attributes:, parent: self, &block)
|
|
66
130
|
end
|
|
67
131
|
|
|
132
|
+
# Forwards this element as the first argument to another receiver.
|
|
133
|
+
# @overload Forward(receiver, method, *args, **kwargs)
|
|
134
|
+
# @param receiver [Object] target receiver
|
|
135
|
+
# @param method [Symbol, String] method name
|
|
136
|
+
# @param args [Array<Object>] additional arguments
|
|
137
|
+
# @param kwargs [Hash] additional keyword arguments
|
|
138
|
+
# @return [Object] forwarded call result
|
|
68
139
|
def Forward(receiver, method, ...)
|
|
69
140
|
receiver.public_send(method, self, ...)
|
|
70
141
|
end
|
|
71
142
|
|
|
143
|
+
# Reports whether this element has the given SVG name.
|
|
144
|
+
# @param name [Symbol, String] SVG name
|
|
145
|
+
# @return [Boolean]
|
|
72
146
|
def Is?(name)
|
|
73
147
|
self.name() == name.to_sym
|
|
74
148
|
end
|
|
75
149
|
|
|
150
|
+
# Removes this element from its parent.
|
|
151
|
+
# @return [Array<Sevgi::Graphics::Element>, nil] parent children after deletion, or nil for root elements
|
|
76
152
|
def Orphan
|
|
77
153
|
parent.children&.delete(self) unless Root?()
|
|
78
154
|
end
|
|
79
155
|
|
|
156
|
+
# Prepends existing elements as children.
|
|
157
|
+
# @param elements [Array<Sevgi::Graphics::Element>] elements to prepend
|
|
158
|
+
# @return [Sevgi::Graphics::Element] self
|
|
80
159
|
def Prepend(*elements)
|
|
81
160
|
tap { elements.each { it.AdoptFirst(self) } }
|
|
82
161
|
end
|
|
83
162
|
|
|
163
|
+
# Returns the root document element.
|
|
164
|
+
# @return [Sevgi::Graphics::Element]
|
|
84
165
|
def Root
|
|
85
166
|
element = self
|
|
86
|
-
element = element.parent
|
|
167
|
+
element = element.parent until element.Root?()
|
|
87
168
|
|
|
88
169
|
element
|
|
89
170
|
end
|
|
90
171
|
|
|
172
|
+
# Reports whether this element is the root document element.
|
|
173
|
+
# @return [Boolean]
|
|
91
174
|
def Root?
|
|
92
175
|
self.class.root?(self)
|
|
93
176
|
end
|
|
94
177
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
178
|
+
# @overload Stay(value)
|
|
179
|
+
# Wraps a traversal return value as a stop token.
|
|
180
|
+
# @param value [Object] value returned from traversal
|
|
181
|
+
# @return [Sevgi::Graphics::Mixtures::Stop]
|
|
182
|
+
def Stay(...) = Stop.new(...)
|
|
183
|
+
|
|
184
|
+
# Traverses the subtree depth-first.
|
|
185
|
+
# @param depth [Integer] starting depth
|
|
186
|
+
# @param leave [Proc, nil] optional leave callback
|
|
187
|
+
# @return [Sevgi::Graphics::Element, Object] self or the value passed through Stay
|
|
188
|
+
# @raise [Sevgi::ArgumentError] when no block is given
|
|
99
189
|
def Traverse(depth = 0, leave = nil, &block)
|
|
100
190
|
ArgumentError.("Block required") unless block
|
|
101
191
|
|
|
102
|
-
|
|
103
|
-
yield(self, depth).tap { return it.value if it.is_a?(Traversal) }
|
|
104
|
-
|
|
105
|
-
children.each { |child| child.Traverse(depth + 1, leave, &block) }
|
|
106
|
-
|
|
107
|
-
leave&.call(self, depth).tap { return it.value if it.is_a?(Traversal) }
|
|
108
|
-
end
|
|
192
|
+
Traversal.call(self, depth, leave, &block)
|
|
109
193
|
end
|
|
110
194
|
|
|
195
|
+
# Traverses ancestors from this element to the root.
|
|
196
|
+
# @param height [Integer] starting height
|
|
197
|
+
# @return [Object, nil] value passed through Stay, or nil
|
|
198
|
+
# @raise [Sevgi::ArgumentError] when no block is given
|
|
111
199
|
def TraverseUp(height = 0, &block)
|
|
112
200
|
ArgumentError.("Block required") unless block
|
|
113
201
|
|
|
114
202
|
element = self
|
|
115
203
|
|
|
116
204
|
loop do
|
|
117
|
-
yield(element, height).tap { return it.value if it.is_a?(
|
|
205
|
+
yield(element, height).tap { return it.value if it.is_a?(Stop) }
|
|
118
206
|
|
|
119
207
|
break if element.Root?()
|
|
120
208
|
|
|
@@ -123,17 +211,50 @@ module Sevgi
|
|
|
123
211
|
end
|
|
124
212
|
end
|
|
125
213
|
|
|
214
|
+
# Evaluates a block in the parent element context.
|
|
215
|
+
# @param args [Array<Object>] optional receiver override followed by block arguments
|
|
216
|
+
# @param kwargs [Hash] keyword arguments passed to the block
|
|
217
|
+
# @return [Sevgi::Graphics::Element] self
|
|
126
218
|
def With(*args, **kwargs, &block)
|
|
127
219
|
tap { (args.shift || self).parent.instance_exec(*args, **kwargs, &block) }
|
|
128
220
|
end
|
|
129
221
|
|
|
222
|
+
# Evaluates a block in this element context.
|
|
223
|
+
# @param args [Array<Object>] optional receiver override followed by block arguments
|
|
224
|
+
# @param kwargs [Hash] keyword arguments passed to the block
|
|
225
|
+
# @return [Sevgi::Graphics::Element] self
|
|
130
226
|
def Within(*args, **kwargs, &block)
|
|
131
227
|
tap { (args.shift || self).instance_exec(*args, **kwargs, &block) }
|
|
132
228
|
end
|
|
133
229
|
|
|
230
|
+
# Appends an element as a child.
|
|
231
|
+
# @param element [Sevgi::Graphics::Element] element to append
|
|
232
|
+
# @return [Sevgi::Graphics::Element] self
|
|
134
233
|
def <<(element)
|
|
135
234
|
Append(element)
|
|
136
235
|
end
|
|
236
|
+
|
|
237
|
+
# Adoption target validation that keeps tree mutation atomic.
|
|
238
|
+
# @api private
|
|
239
|
+
module Adoption
|
|
240
|
+
# Rejects target parents that would create a cycle.
|
|
241
|
+
# @param element [Sevgi::Graphics::Element] element being moved
|
|
242
|
+
# @param parent [Sevgi::Graphics::Element, Object] target parent
|
|
243
|
+
# @return [void]
|
|
244
|
+
# @raise [Sevgi::ArgumentError] when the target parent is this element or one of its descendants
|
|
245
|
+
def self.validate(element, parent)
|
|
246
|
+
ArgumentError.("Element cannot be adopted under itself") if parent.equal?(element)
|
|
247
|
+
|
|
248
|
+
while parent.respond_to?(:Root?)
|
|
249
|
+
ArgumentError.("Element cannot be adopted under its descendant") if parent.equal?(element)
|
|
250
|
+
break if parent.Root?()
|
|
251
|
+
|
|
252
|
+
parent = parent.parent
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private_constant :Adoption
|
|
137
258
|
end
|
|
138
259
|
end
|
|
139
260
|
end
|
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
+
# DSL helpers for duplicating independent element subtrees.
|
|
6
7
|
module Duplicate
|
|
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. Public `id` attributes are
|
|
10
|
+
# moved to an internal `-id` attribute before the optional block runs, allowing the block to derive replacement
|
|
11
|
+
# ids without rendering duplicate public ids.
|
|
12
|
+
# @param dx [Numeric, nil] x translation
|
|
13
|
+
# @param dy [Numeric, nil] y translation
|
|
14
|
+
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
15
|
+
# @yield [element] optional customization hook for each copied element
|
|
16
|
+
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
17
|
+
# @return [Sevgi::Graphics::Element] duplicated element
|
|
18
|
+
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
7
19
|
def Duplicate(dx: nil, dy: nil, parent: nil, &block)
|
|
8
|
-
duplicated =
|
|
20
|
+
duplicated = Subtree.copy(self)
|
|
9
21
|
|
|
10
22
|
duplicated.Traverse() do |element|
|
|
11
|
-
|
|
12
|
-
id = (element.attributes = element.attributes.dup).delete(:id)
|
|
23
|
+
id = element.attributes.delete(:id)
|
|
13
24
|
element[:"#{ATTRIBUTE_INTERNAL_PREFIX}id"] = id if id
|
|
14
25
|
block&.call(element)
|
|
15
26
|
end
|
|
@@ -21,9 +32,42 @@ module Sevgi
|
|
|
21
32
|
duplicated
|
|
22
33
|
end
|
|
23
34
|
|
|
35
|
+
# Duplicates an element subtree along the x-axis.
|
|
36
|
+
# @param dx [Numeric] x translation
|
|
37
|
+
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
38
|
+
# @yield [element] optional customization hook for each copied element
|
|
39
|
+
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
40
|
+
# @return [Sevgi::Graphics::Element] duplicated element
|
|
41
|
+
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
24
42
|
def DuplicateX(dx, parent: nil, &block) = Duplicate(dx:, dy: 0, parent:, &block)
|
|
25
43
|
|
|
44
|
+
# Duplicates an element subtree along the y-axis.
|
|
45
|
+
# @param dy [Numeric] y translation
|
|
46
|
+
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
47
|
+
# @yield [element] optional customization hook for each copied element
|
|
48
|
+
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
49
|
+
# @return [Sevgi::Graphics::Element] duplicated element
|
|
50
|
+
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
26
51
|
def DuplicateY(dy, parent: nil, &block) = Duplicate(dx: 0, dy:, parent:, &block)
|
|
52
|
+
|
|
53
|
+
# Recursive subtree copier that keeps duplicate implementation state out of the DSL surface.
|
|
54
|
+
# @api private
|
|
55
|
+
module Subtree
|
|
56
|
+
# Builds an independent copy of an element subtree.
|
|
57
|
+
# @param element [Sevgi::Graphics::Element] source subtree root
|
|
58
|
+
# @param parent [Sevgi::Graphics::Element, Object] parent for the copied root
|
|
59
|
+
# @return [Sevgi::Graphics::Element] copied subtree root
|
|
60
|
+
def self.copy(element, parent = element.parent)
|
|
61
|
+
element.dup.tap do |duplicated|
|
|
62
|
+
duplicated.send(:parent=, parent)
|
|
63
|
+
duplicated.send(:attributes=, element.attributes.dup)
|
|
64
|
+
duplicated.send(:contents=, element.contents.dup)
|
|
65
|
+
duplicated.send(:children=, element.children.map { |child| copy(child, duplicated) })
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private_constant :Subtree
|
|
27
71
|
end
|
|
28
72
|
end
|
|
29
73
|
end
|
|
@@ -3,21 +3,46 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
+
# DSL helpers for native SVG export formats.
|
|
6
7
|
module Export
|
|
8
|
+
# Exports the document as PDF.
|
|
9
|
+
# @param path [String, nil] output path or directory
|
|
10
|
+
# @param kwargs [Hash] export options
|
|
11
|
+
# @return [String] output path
|
|
12
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
13
|
+
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
14
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
7
15
|
def PDF(path = nil, **kwargs, &block)
|
|
8
|
-
|
|
16
|
+
begin
|
|
17
|
+
require "sevgi/sundries"
|
|
18
|
+
|
|
19
|
+
rescue ::LoadError => e
|
|
20
|
+
raise unless e.path == "sevgi/sundries"
|
|
21
|
+
|
|
22
|
+
MissingComponentError.("sevgi/sundries")
|
|
23
|
+
end
|
|
9
24
|
|
|
10
25
|
Export(path, **kwargs, format: :pdf, &block)
|
|
11
|
-
rescue ::LoadError
|
|
12
|
-
raise NoMethodError, "\"sevgi/sundries\" required"
|
|
13
26
|
end
|
|
14
27
|
|
|
28
|
+
# Exports the document as PNG.
|
|
29
|
+
# @param path [String, nil] output path or directory
|
|
30
|
+
# @param kwargs [Hash] export options
|
|
31
|
+
# @return [String] output path
|
|
32
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
33
|
+
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
34
|
+
# @raise [Sevgi::Sundries::Export::ExportError] when native export fails
|
|
15
35
|
def PNG(path = nil, **kwargs, &block)
|
|
16
|
-
|
|
36
|
+
begin
|
|
37
|
+
require "sevgi/sundries"
|
|
38
|
+
|
|
39
|
+
rescue ::LoadError => e
|
|
40
|
+
raise unless e.path == "sevgi/sundries"
|
|
41
|
+
|
|
42
|
+
MissingComponentError.("sevgi/sundries")
|
|
43
|
+
end
|
|
17
44
|
|
|
18
45
|
Export(path, **kwargs, format: :png, &block)
|
|
19
|
-
rescue ::LoadError
|
|
20
|
-
raise NoMethodError, "\"sevgi/sundries\" required"
|
|
21
46
|
end
|
|
22
47
|
|
|
23
48
|
private
|
|
@@ -3,14 +3,35 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
+
# DSL helpers for drawing geometry-derived hatch lines.
|
|
6
7
|
module Hatch
|
|
8
|
+
# Draws one or more geometry line-like objects into this element.
|
|
9
|
+
# @param lines [Object, Array<Object>] drawable geometry objects
|
|
10
|
+
# @param kwargs [Hash] SVG attributes passed to each draw call
|
|
11
|
+
# @return [Array<Sevgi::Graphics::Element>] rendered line elements
|
|
7
12
|
def Draw(lines, **kwargs)
|
|
8
13
|
Array(lines).map { it.draw(self, **kwargs) }
|
|
9
14
|
end
|
|
10
15
|
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
# Draws hatch lines swept through a geometry element.
|
|
17
|
+
# @param element [Object] geometry element responding to position
|
|
18
|
+
# @param angle [Numeric] hatch angle in degrees
|
|
19
|
+
# @param step [Numeric] distance between hatch lines
|
|
20
|
+
# @param initial [Object, nil] initial point for the sweep
|
|
21
|
+
# @param kwargs [Hash] SVG attributes passed to each draw call
|
|
22
|
+
# @return [Array<Sevgi::Graphics::Element>] rendered line elements
|
|
23
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/geometry is unavailable
|
|
24
|
+
def Hatch(element, angle:, step:, initial: nil, **kwargs)
|
|
25
|
+
begin
|
|
26
|
+
require "sevgi/geometry"
|
|
27
|
+
|
|
28
|
+
rescue ::LoadError => e
|
|
29
|
+
raise unless e.path == "sevgi/geometry"
|
|
30
|
+
|
|
31
|
+
MissingComponentError.("sevgi/geometry")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Draw(Geometry::Operation.sweep!(element, initial: initial || element.position, angle:, step:), **kwargs)
|
|
14
35
|
end
|
|
15
36
|
end
|
|
16
37
|
end
|
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
+
# DSL helpers for collecting and hiding SVG ids.
|
|
6
7
|
module Identify
|
|
8
|
+
# Index of element ids under a subtree.
|
|
7
9
|
class Identifiers
|
|
8
|
-
|
|
10
|
+
# @return [Sevgi::Graphics::Element] indexed root element
|
|
11
|
+
attr_reader :element
|
|
9
12
|
|
|
13
|
+
# @return [Hash<String, Sevgi::Graphics::Element>] id namespace
|
|
14
|
+
attr_reader :namespace
|
|
15
|
+
|
|
16
|
+
# @return [Hash<String, Array<Sevgi::Graphics::Element>>] duplicate id groups
|
|
17
|
+
attr_reader :collision
|
|
18
|
+
|
|
19
|
+
# Builds an id index for an element subtree.
|
|
20
|
+
# @param element [Sevgi::Graphics::Element] root element
|
|
21
|
+
# @return [void]
|
|
10
22
|
def initialize(element)
|
|
11
23
|
@element = element
|
|
12
24
|
@namespace = {}
|
|
@@ -15,10 +27,16 @@ module Sevgi
|
|
|
15
27
|
build
|
|
16
28
|
end
|
|
17
29
|
|
|
30
|
+
# Reports whether duplicate ids were found.
|
|
31
|
+
# @return [Boolean]
|
|
18
32
|
def conflict?
|
|
19
33
|
!@collision.empty?
|
|
20
34
|
end
|
|
21
35
|
|
|
36
|
+
# @overload [](id)
|
|
37
|
+
# Returns the element registered for an id.
|
|
38
|
+
# @param id [String] SVG id
|
|
39
|
+
# @return [Sevgi::Graphics::Element, nil]
|
|
22
40
|
def [](*)
|
|
23
41
|
@namespace[*]
|
|
24
42
|
end
|
|
@@ -27,7 +45,9 @@ module Sevgi
|
|
|
27
45
|
|
|
28
46
|
def build
|
|
29
47
|
element.Traverse() do |element|
|
|
30
|
-
next unless (
|
|
48
|
+
next unless (value = element[:id])
|
|
49
|
+
|
|
50
|
+
id = Attribute.xml_text(value)
|
|
31
51
|
|
|
32
52
|
if @namespace.key?(id)
|
|
33
53
|
(@collision[id] ||= [@namespace[id]]) << element
|
|
@@ -38,6 +58,8 @@ module Sevgi
|
|
|
38
58
|
end
|
|
39
59
|
end
|
|
40
60
|
|
|
61
|
+
# Moves visible id attributes to Sevgi-internal id storage.
|
|
62
|
+
# @return [Sevgi::Graphics::Element] self
|
|
41
63
|
def Disidentify
|
|
42
64
|
Traverse do |element|
|
|
43
65
|
next unless element[:id]
|
|
@@ -46,6 +68,8 @@ module Sevgi
|
|
|
46
68
|
end
|
|
47
69
|
end
|
|
48
70
|
|
|
71
|
+
# Builds an id index for this element subtree.
|
|
72
|
+
# @return [Sevgi::Graphics::Mixtures::Identify::Identifiers]
|
|
49
73
|
def Identifiers = Identifiers.new(self)
|
|
50
74
|
end
|
|
51
75
|
end
|
|
@@ -3,14 +3,50 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
+
# DSL helpers for including derendered SVG/XML fragments.
|
|
6
7
|
module Include
|
|
7
8
|
require "sevgi/derender"
|
|
8
9
|
|
|
10
|
+
# Includes a derendered node matching an id.
|
|
11
|
+
#
|
|
12
|
+
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
13
|
+
# @param file [String] source SVG/XML file
|
|
14
|
+
# @param id [String, Symbol] source node id
|
|
15
|
+
# @return [Sevgi::Graphics::Element, nil] included element, or nil when the selected node produces no graphics
|
|
16
|
+
# output
|
|
17
|
+
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
18
|
+
# absent
|
|
19
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
9
20
|
def Include(file, id) = Derender.evaluate_file(file, self, id:)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
|
|
22
|
+
# Includes the children of a derendered node matching an id.
|
|
23
|
+
#
|
|
24
|
+
# SVG/XML file content is treated as data and is not evaluated as Ruby source.
|
|
25
|
+
# @param file [String] source SVG/XML file
|
|
26
|
+
# @param id [String, Symbol] source node id
|
|
27
|
+
# @return [Array<Sevgi::Graphics::Element>] included children
|
|
28
|
+
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
29
|
+
# absent
|
|
30
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
31
|
+
def IncludeChildren(file, id) = Derender.evaluate_file_children(file, self, id:)
|
|
32
|
+
rescue ::LoadError => e
|
|
33
|
+
raise unless e.path == "sevgi/derender"
|
|
34
|
+
|
|
35
|
+
# @overload IncludeChildren(file, id)
|
|
36
|
+
# Raises because sevgi/derender is unavailable.
|
|
37
|
+
# @param file [String] source SVG/XML file
|
|
38
|
+
# @param id [String, Symbol] source node id
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @raise [Sevgi::MissingComponentError] always
|
|
41
|
+
def IncludeChildren(...) = MissingComponentError.("sevgi/derender")
|
|
42
|
+
|
|
43
|
+
# @overload Include(file, id)
|
|
44
|
+
# Raises because sevgi/derender is unavailable.
|
|
45
|
+
# @param file [String] source SVG/XML file
|
|
46
|
+
# @param id [String, Symbol] source node id
|
|
47
|
+
# @return [void]
|
|
48
|
+
# @raise [Sevgi::MissingComponentError] always
|
|
49
|
+
def Include(...) = MissingComponentError.("sevgi/derender")
|
|
14
50
|
end
|
|
15
51
|
end
|
|
16
52
|
end
|