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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +5 -0
  4. data/README.md +37 -0
  5. data/lib/sevgi/graphics/attribute.rb +82 -10
  6. data/lib/sevgi/graphics/auxiliary/canvas.rb +153 -0
  7. data/lib/sevgi/graphics/auxiliary/content.rb +157 -0
  8. data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
  9. data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
  10. data/lib/sevgi/graphics/auxiliary.rb +7 -0
  11. data/lib/sevgi/graphics/document/base.rb +6 -0
  12. data/lib/sevgi/graphics/document/default.rb +1 -0
  13. data/lib/sevgi/graphics/document/html.rb +1 -0
  14. data/lib/sevgi/graphics/document/inkscape.rb +1 -0
  15. data/lib/sevgi/graphics/document/minimal.rb +1 -0
  16. data/lib/sevgi/graphics/document.rb +205 -13
  17. data/lib/sevgi/graphics/element.rb +69 -9
  18. data/lib/sevgi/graphics/mixtures/call.rb +62 -4
  19. data/lib/sevgi/graphics/mixtures/core.rb +146 -25
  20. data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -3
  21. data/lib/sevgi/graphics/mixtures/export.rb +31 -6
  22. data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
  23. data/lib/sevgi/graphics/mixtures/identify.rb +26 -2
  24. data/lib/sevgi/graphics/mixtures/include.rb +40 -4
  25. data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
  26. data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
  27. data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
  28. data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
  29. data/lib/sevgi/graphics/mixtures/render.rb +136 -14
  30. data/lib/sevgi/graphics/mixtures/save.rb +21 -3
  31. data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
  32. data/lib/sevgi/graphics/mixtures/tile.rb +52 -9
  33. data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
  34. data/lib/sevgi/graphics/mixtures/underscore.rb +21 -1
  35. data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
  36. data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
  37. data/lib/sevgi/graphics/mixtures.rb +35 -3
  38. data/lib/sevgi/graphics/version.rb +2 -1
  39. data/lib/sevgi/graphics.rb +71 -5
  40. metadata +12 -9
  41. data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
  42. data/lib/sevgi/graphics/auxilary/content.rb +0 -81
  43. data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
  44. data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
  45. 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
- def call(after = false, &block) = ((after ? _after : _befores) << block)
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
- raise ArgumentError, "Must be a module: #{mod}" unless mod.instance_of?(::Module)
77
+ ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
42
78
 
43
- (mod.respond_to?(:_callables) ? mod._callables : mod.instance_methods).map { mod.instance_method(it) }
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
- raise ArgumentError, "Must be a module: #{mod}" unless mod.instance_of?(::Module)
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
- unless self[:class]
39
- self[:class] = classes
40
- next
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
- case self[:class]
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 self[key]
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 while element.Root?()
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
- Traversal = Data.define(:value)
96
-
97
- def Stay(...) = Traversal.new(...)
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
- tap do
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?(Traversal) }
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 = dup
20
+ duplicated = Subtree.copy(self)
9
21
 
10
22
  duplicated.Traverse() do |element|
11
- element.children = element.children.map(&:dup)
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
- require "sevgi/sundries"
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
- require "sevgi/sundries"
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
- # TODO: angle vs direction
12
- def Hatch(element, direction:, step:, initial: nil, **kwargs)
13
- Draw(Geometry::Operation.sweep!(element, initial: initial || element.position, direction:, step:), **kwargs)
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
- attr_reader :element, :namespace, :collision
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 (id = element[:id])
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
- def Include!(file, id) = Derender.evaluate_file!(file, self, id:)
11
- rescue ::LoadError
12
- def Include(...) = raise NoMethodError, "\"sevgi/derender\" required"
13
- def Include!(...) = raise NoMethodError, "\"sevgi/derender\" required"
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