sevgi-graphics 0.73.2 → 0.93.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -0
  3. data/lib/sevgi/graphics/attribute.rb +58 -6
  4. data/lib/sevgi/graphics/auxiliary/canvas.rb +125 -0
  5. data/lib/sevgi/graphics/auxiliary/content.rb +145 -0
  6. data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
  7. data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
  8. data/lib/sevgi/graphics/auxiliary.rb +7 -0
  9. data/lib/sevgi/graphics/document/base.rb +6 -0
  10. data/lib/sevgi/graphics/document/default.rb +1 -0
  11. data/lib/sevgi/graphics/document/html.rb +1 -0
  12. data/lib/sevgi/graphics/document/inkscape.rb +1 -0
  13. data/lib/sevgi/graphics/document/minimal.rb +1 -0
  14. data/lib/sevgi/graphics/document.rb +137 -11
  15. data/lib/sevgi/graphics/element.rb +68 -8
  16. data/lib/sevgi/graphics/mixtures/call.rb +35 -3
  17. data/lib/sevgi/graphics/mixtures/core.rb +107 -18
  18. data/lib/sevgi/graphics/mixtures/duplicate.rb +14 -0
  19. data/lib/sevgi/graphics/mixtures/export.rb +29 -6
  20. data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
  21. data/lib/sevgi/graphics/mixtures/identify.rb +23 -1
  22. data/lib/sevgi/graphics/mixtures/include.rb +31 -4
  23. data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
  24. data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
  25. data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
  26. data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
  27. data/lib/sevgi/graphics/mixtures/render.rb +90 -2
  28. data/lib/sevgi/graphics/mixtures/save.rb +21 -3
  29. data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
  30. data/lib/sevgi/graphics/mixtures/tile.rb +41 -6
  31. data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
  32. data/lib/sevgi/graphics/mixtures/underscore.rb +10 -1
  33. data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
  34. data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
  35. data/lib/sevgi/graphics/mixtures.rb +35 -3
  36. data/lib/sevgi/graphics/version.rb +2 -1
  37. data/lib/sevgi/graphics.rb +57 -5
  38. metadata +9 -8
  39. data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
  40. data/lib/sevgi/graphics/auxilary/content.rb +0 -81
  41. data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
  42. data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
  43. data/lib/sevgi/graphics/auxilary.rb +0 -7
@@ -3,39 +3,68 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for RDF and license metadata.
6
7
  module RDF
8
+ # Adds RDF license metadata inside a metadata element.
9
+ # @param kwargs [Hash] RDF work options
10
+ # @return [Sevgi::Graphics::Element] metadata element
7
11
  def License(**kwargs, &block) = metadata { RDFWork(**kwargs, &block) }
8
12
 
9
13
  # Use SPDX license codes in underscored form: https://spdx.org/licenses/
10
14
  #
15
+ # Adds Creative Commons BY-SA license metadata.
16
+ # @param kwargs [Hash] RDF work options
17
+ # @return [Sevgi::Graphics::Element] metadata element
11
18
  def License_CC_BY_SA(**kwargs, &block)
12
19
  License(**kwargs, license: "https://creativecommons.org/licenses/by-sa/4.0/", &block)
13
20
  end
14
21
 
22
+ # Adds Creative Commons BY-NC license metadata.
23
+ # @param kwargs [Hash] RDF work options
24
+ # @return [Sevgi::Graphics::Element] metadata element
15
25
  def License_CC_BY_NC(**kwargs, &block)
16
26
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc/4.0/", &block)
17
27
  end
18
28
 
29
+ # Adds Creative Commons BY-NC-SA license metadata.
30
+ # @param kwargs [Hash] RDF work options
31
+ # @return [Sevgi::Graphics::Element] metadata element
19
32
  def License_CC_BY_NC_SA(**kwargs, &block)
20
33
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-sa/4.0/", &block)
21
34
  end
22
35
 
36
+ # Adds Creative Commons BY-ND license metadata.
37
+ # @param kwargs [Hash] RDF work options
38
+ # @return [Sevgi::Graphics::Element] metadata element
23
39
  def License_CC_BY_ND(**kwargs, &block)
24
40
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nd/4.0/", &block)
25
41
  end
26
42
 
43
+ # Adds Creative Commons BY-NC-ND license metadata.
44
+ # @param kwargs [Hash] RDF work options
45
+ # @return [Sevgi::Graphics::Element] metadata element
27
46
  def License_CC_BY_NC_ND(**kwargs, &block)
28
47
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-nd/4.0/", &block)
29
48
  end
30
49
 
50
+ # Adds Creative Commons Zero metadata.
51
+ # @param kwargs [Hash] RDF work options
52
+ # @return [Sevgi::Graphics::Element] metadata element
31
53
  def License_CC0(**kwargs, &block)
32
54
  License(**kwargs, license: "https://creativecommons.org/publicdomain/zero/1.0/", &block)
33
55
  end
34
56
 
57
+ # Adds Free Art License metadata.
58
+ # @param kwargs [Hash] RDF work options
59
+ # @return [Sevgi::Graphics::Element] metadata element
35
60
  def License_LAL(**kwargs, &block) = License(**kwargs, license: "https://artlibre.org/licence/lal/en/", &block)
36
61
 
62
+ # Builds an RDF root element.
63
+ # @param _kwargs [Hash] currently unused options
64
+ # @return [Sevgi::Graphics::Element] RDF element
65
+ # @raise [Sevgi::ArgumentError] when no block is given
37
66
  def RDF(**_kwargs, &block)
38
- raise ArgumentError, "Block required" unless block
67
+ ArgumentError.("Block required") unless block
39
68
 
40
69
  Element(
41
70
  :"rdf:RDF",
@@ -48,6 +77,16 @@ module Sevgi
48
77
  end
49
78
 
50
79
  # rubocop:disable Metrics/MethodLength
80
+ # Builds a Creative Commons RDF Work element.
81
+ # @param kwargs [Hash] RDF work options
82
+ # @option kwargs [String] :title work title
83
+ # @option kwargs [String] :description work description
84
+ # @option kwargs [String] :creator creator
85
+ # @option kwargs [String] :publisher publisher
86
+ # @option kwargs [String] :date date
87
+ # @option kwargs [String] :language language
88
+ # @option kwargs [String] :license license URL
89
+ # @return [Sevgi::Graphics::Element] RDF element
51
90
  def RDFWork(**kwargs, &block)
52
91
  RDF do
53
92
  Element(:"cc:Work", "rdf:about": "") do
@@ -3,18 +3,36 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for rendering SVG documents and children.
6
7
  module Render
8
+ # SVG source renderer.
9
+ # @api private
7
10
  class Renderer
11
+ # Default renderer options.
8
12
  DEFAULTS = {indent: " ", linelength: 140, style: :hybrid}.freeze
9
13
 
14
+ # Attribute rendering strategies.
15
+ # @api private
10
16
  module Attributes
17
+ # Block-style attribute renderer.
18
+ # @api private
11
19
  module Block
20
+ # Renders attributes in block form.
21
+ # @param element [Sevgi::Graphics::Element] rendered element
22
+ # @param depth [Integer] element depth
23
+ # @return [void]
12
24
  def attributes(element, depth)
13
25
  attributes_block(element, depth, element.attributes.to_xml_lines)
14
26
  end
15
27
  end
16
28
 
29
+ # Hybrid attribute renderer.
30
+ # @api private
17
31
  module Hybrid
32
+ # Renders attributes inline or in block form according to line length.
33
+ # @param element [Sevgi::Graphics::Element] rendered element
34
+ # @param depth [Integer] element depth
35
+ # @return [void]
18
36
  def attributes(element, depth)
19
37
  if attributes_as_block?(lines = element.attributes.to_xml_lines, depth)
20
38
  attributes_block(element, depth, lines)
@@ -23,16 +41,30 @@ module Sevgi
23
41
  end
24
42
  end
25
43
 
44
+ # Reports whether attributes should be rendered in block form.
45
+ # @param lines [Array<String>] rendered attribute lines
46
+ # @param depth [Integer] element depth
47
+ # @return [Boolean]
26
48
  def attributes_as_block?(lines, depth)
27
49
  linelength(lines, depth) > options[:linelength]
28
50
  end
29
51
 
52
+ # Returns the effective inline line length.
53
+ # @param lines [Array<String>] rendered attribute lines
54
+ # @param depth [Integer] element depth
55
+ # @return [Integer]
30
56
  def linelength(lines, depth)
31
57
  indent(depth).length + lines.sum(&:length)
32
58
  end
33
59
  end
34
60
 
61
+ # Inline attribute renderer.
62
+ # @api private
35
63
  module Inline
64
+ # Renders attributes inline.
65
+ # @param element [Sevgi::Graphics::Element] rendered element
66
+ # @param depth [Integer] element depth
67
+ # @return [void]
36
68
  def attributes(element, depth)
37
69
  attributes_inline(element, depth, element.attributes.to_xml_lines)
38
70
  end
@@ -47,17 +79,41 @@ module Sevgi
47
79
 
48
80
  private_constant :ELEMENTS_WITH_INLINE_CONTENT, :ELEMENTS_WITH_BLOCK_CONTENT, :SEPARATOR
49
81
 
50
- attr_reader :root, :options, :output
82
+ # @return [Sevgi::Graphics::Element] root element
83
+ attr_reader :root
51
84
 
85
+ # @return [Hash] renderer options
86
+ attr_reader :options
87
+
88
+ # @return [Array<Array<String>>] buffered output lines
89
+ attr_reader :output
90
+
91
+ # Inline-content output splice helper.
92
+ # @api private
52
93
  class Inlines
94
+ # Inline mark with start, stop, and depth.
53
95
  Mark = Struct.new(:start, :stop, :depth)
54
96
 
97
+ # Creates an inline splice tracker.
98
+ # @return [void]
55
99
  def initialize = @marks = []
56
100
 
101
+ # Starts an inline splice range.
102
+ # @param index [Integer] output index
103
+ # @param depth [Integer] element depth
104
+ # @return [Array<Sevgi::Graphics::Mixtures::Render::Renderer::Inlines::Mark>]
57
105
  def start(index, depth) = @marks << Mark.new(start: index, depth:)
58
106
 
107
+ # Ends the latest inline splice range.
108
+ # @param index [Integer] output index
109
+ # @return [Integer]
59
110
  def stop(index) = @marks.last.stop = index
60
111
 
112
+ # Joins marked output ranges into inline content.
113
+ # @param output [Array<Array<String>, nil>] renderer output buffer
114
+ # @param indent [String] indentation unit
115
+ # @param separator [String] line separator
116
+ # @return [Array<Array<String>, nil>, nil]
61
117
  def join(output, indent:, separator:)
62
118
  return if @marks.empty?
63
119
 
@@ -75,6 +131,15 @@ module Sevgi
75
131
  end
76
132
  end
77
133
 
134
+ # @overload initialize(root, **options)
135
+ # Creates a renderer.
136
+ # @param root [Sevgi::Graphics::Element] root element
137
+ # @param options [Hash] renderer options
138
+ # @option options [String] :indent indentation unit
139
+ # @option options [Integer] :linelength hybrid attribute line length
140
+ # @option options [Symbol] :style attribute style, one of :hybrid, :inline, or :block
141
+ # @return [void]
142
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
78
143
  def initialize(root, **)
79
144
  @root = root
80
145
  @options = DEFAULTS.merge(**)
@@ -84,14 +149,27 @@ module Sevgi
84
149
  build
85
150
  end
86
151
 
152
+ # @overload call(root, **options)
153
+ # Renders a root element.
154
+ # @param root [Sevgi::Graphics::Element] root element
155
+ # @param options [Hash] renderer options
156
+ # @return [String] SVG source
157
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
87
158
  def self.call(root, **) = new(root, **).call(*root.class.preambles)
88
159
 
160
+ # Appends rendered lines to the output buffer.
161
+ # @param depth [Integer, nil] indentation depth
162
+ # @param lines [Array<String>] rendered lines
163
+ # @return [Array<Array<String>>, nil]
89
164
  def append(depth, *lines)
90
165
  return if lines.empty?
91
166
 
92
167
  output.append(lines.map { "#{indent(depth) if depth}#{it}" })
93
168
  end
94
169
 
170
+ # Renders the document.
171
+ # @param preambles [Array<String>] preamble lines
172
+ # @return [String] SVG source
95
173
  def call(*preambles)
96
174
  output.append(preambles) unless preambles.empty?
97
175
 
@@ -108,6 +186,8 @@ module Sevgi
108
186
  attr_reader :inlines
109
187
 
110
188
  def attributes_block(element, depth, lines)
189
+ return attributes_inline(element, depth, lines) if lines.empty?
190
+
111
191
  append(depth, "<#{element.name}")
112
192
  append(depth + 1, *lines)
113
193
  append(depth, ">") unless childless?(element)
@@ -202,9 +282,17 @@ module Sevgi
202
282
 
203
283
  private_constant :Renderer
204
284
 
285
+ # @overload Render(**options)
286
+ # Renders this element as SVG source.
287
+ # @param options [Hash] renderer options
288
+ # @return [String] SVG source
289
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
205
290
  def Render(**) = Renderer.(self, **)
206
291
 
207
- def Render!(separator = SEPARATOR * 2) = children.map(&:Render!).join(separator)
292
+ # Renders only this element's children.
293
+ # @param separator [String] separator between child documents
294
+ # @return [String] rendered children
295
+ def RenderChildren(separator = "\n\n") = children.map(&:Render).join(separator)
208
296
  end
209
297
  end
210
298
  end
@@ -5,13 +5,23 @@ require "fileutils"
5
5
  module Sevgi
6
6
  module Graphics
7
7
  module Mixtures
8
+ # DSL helpers for writing rendered SVG output.
8
9
  module Save
10
+ # Default SVG extension.
9
11
  EXT = ".svg"
10
12
 
11
- def Out(*, **, &filter)
12
- F.out(self.(**), *, &filter)
13
+ # Writes rendered SVG to standard output.
14
+ # @param kwargs [Hash] render options
15
+ # @return [Object] F.out return value
16
+ def Out(**kwargs, &filter)
17
+ F.out(self.(**kwargs), &filter)
13
18
  end
14
19
 
20
+ # Saves rendered SVG to a path derived from the caller by default.
21
+ # @param path [String, nil] output path or directory
22
+ # @param default [String, nil] default output path
23
+ # @param backup_suffix [String, nil] suffix used for an existing-file backup
24
+ # @return [Object] F.out return value
15
25
  def Save(path = nil, default: nil, backup_suffix: nil, &filter)
16
26
  default ||= F.subext(EXT, caller_locations(1..1).first.path)
17
27
 
@@ -26,7 +36,15 @@ module Sevgi
26
36
  ::FileUtils.cp(path, "#{path}#{backup_suffix}")
27
37
  end
28
38
 
29
- Out(path, &filter)
39
+ Write(path, &filter)
40
+ end
41
+
42
+ # Writes rendered SVG to a path.
43
+ # @param path [String] output path
44
+ # @param kwargs [Hash] render options
45
+ # @return [Object] F.out return value
46
+ def Write(path, **kwargs, &filter)
47
+ F.out(self.(**kwargs), path, &filter)
30
48
  end
31
49
  end
32
50
  end
@@ -3,7 +3,14 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for expanding callable modules into SVG symbols.
6
7
  module Symbols
8
+ # Renders module callables as symbols under defs.
9
+ # @param mod [Module] callable drawing module
10
+ # @param args [Array<Object>] callable arguments
11
+ # @param kwargs [Hash] defs attributes
12
+ # @return [Sevgi::Graphics::Element] defs element
13
+ # @raise [Sevgi::ArgumentError] when mod is not a plain module
7
14
  def Symbols(mod, *args, **kwargs, &block)
8
15
  CallWithin(mod, :defs, :symbol, *args, **kwargs) do |name, element|
9
16
  element[:id] = block ? block.call(name) : name.to_s.split("_").join("-")
@@ -4,9 +4,22 @@ module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
6
  # rubocop:disable Metrics/MethodLength
7
+ # DSL helpers for repeated SVG use elements.
7
8
  module Tile
9
+ # Prefix used for generated tile CSS classes.
8
10
  PREFIX = "tile"
9
11
 
12
+ # Builds a two-dimensional tile grid.
13
+ # @param id [String] referenced template id
14
+ # @param nx [Integer] number of columns
15
+ # @param dx [Numeric] horizontal spacing
16
+ # @param ox [Numeric] horizontal offset
17
+ # @param ny [Integer] number of rows
18
+ # @param dy [Numeric] vertical spacing
19
+ # @param oy [Numeric] vertical offset
20
+ # @param proc [Proc, nil] optional coordinate/customization proc
21
+ # @return [Sevgi::Graphics::Element] self
22
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
10
23
  def Tile(
11
24
  id = Undefined,
12
25
  nx: Undefined,
@@ -48,8 +61,14 @@ module Sevgi
48
61
  end
49
62
  end
50
63
 
51
- def Tile!(id, klass = PREFIX, ...) = g(id:, class: klass) { Tile(id, ...) }
52
-
64
+ # Builds a one-dimensional horizontal tile row.
65
+ # @param id [String] referenced template id
66
+ # @param n [Integer] number of instances
67
+ # @param d [Numeric] horizontal spacing
68
+ # @param o [Numeric] horizontal offset
69
+ # @param proc [Proc, nil] optional coordinate/customization proc
70
+ # @return [Sevgi::Graphics::Element] self
71
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
53
72
  def TileX(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
54
73
  Helper.assert(id:, n:, d:, o:, proc:)
55
74
 
@@ -75,8 +94,14 @@ module Sevgi
75
94
  end
76
95
  end
77
96
 
78
- def TileX!(id, klass = PREFIX, ...) = g(id:, class: klass) { TileX(id, ...) }
79
-
97
+ # Builds a one-dimensional vertical tile column.
98
+ # @param id [String] referenced template id
99
+ # @param n [Integer] number of instances
100
+ # @param d [Numeric] vertical spacing
101
+ # @param o [Numeric] vertical offset
102
+ # @param proc [Proc, nil] optional coordinate/customization proc
103
+ # @return [Sevgi::Graphics::Element] self
104
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
80
105
  def TileY(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
81
106
  Helper.assert(id:, n:, d:, o:, proc:)
82
107
 
@@ -102,11 +127,12 @@ module Sevgi
102
127
  end
103
128
  end
104
129
 
105
- def TileY!(id, klass = PREFIX, ...) = g(id:, class: klass) { TileY(id, ...) }
106
-
130
+ # Tile argument validation and class helpers.
131
+ # @api private
107
132
  module Helper
108
133
  extend self
109
134
 
135
+ # Argument validators for tile helpers.
110
136
  ASSERTION = {
111
137
  id: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
112
138
  n: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
@@ -121,6 +147,10 @@ module Sevgi
121
147
  proc: proc { |name, value| "Argument '#{name}' must be a proc" unless value.nil? || value.is_a?(::Proc) }
122
148
  }.freeze
123
149
 
150
+ # Validates tile arguments.
151
+ # @param kwargs [Hash] tile arguments
152
+ # @return [nil]
153
+ # @raise [Sevgi::ArgumentError] when an argument is missing or invalid
124
154
  def assert(**kwargs)
125
155
  kwargs.each do |name, value|
126
156
  issue = "Argument '#{name}' required" if value == Undefined
@@ -135,6 +165,11 @@ module Sevgi
135
165
  # rubocop:enable Metrics/MethodLength
136
166
  end
137
167
 
168
+ # Returns positional tile CSS classes.
169
+ # @param as [String] class axis label
170
+ # @param index [Integer] zero-based index
171
+ # @param upper [Integer] item count
172
+ # @return [Array<String>]
138
173
  def classify(as:, index:, upper:)
139
174
  [].tap do |classes|
140
175
  classes << "#{PREFIX}-#{as}-#{index + 1}"
@@ -3,7 +3,14 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for SVG transform attributes.
6
7
  module Transform
8
+ # Aligns an inner box inside an outer box.
9
+ # @param position [Symbol, String, nil] alignment name
10
+ # @param inner [#width, #height, nil] inner box
11
+ # @param outer [#width, #height, nil] outer box
12
+ # @return [Sevgi::Graphics::Element] self
13
+ # @raise [Sevgi::ArgumentError] when alignment is unsupported
7
14
  def Align(position, inner:, outer:)
8
15
  return self unless position && inner && outer
9
16
 
@@ -15,34 +22,47 @@ module Sevgi
15
22
  end
16
23
  end
17
24
 
25
+ # Appends a scale(-1, -1) transform.
26
+ # @return [Sevgi::Graphics::Element] self
18
27
  def Flip
19
28
  tap do
20
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(-1, 1) scale(1, 1)"
29
+ attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(-1, -1)"
21
30
  end
22
31
  end
23
32
 
33
+ # Appends a horizontal flip transform.
34
+ # @return [Sevgi::Graphics::Element] self
24
35
  def FlipX
25
36
  tap do
26
37
  attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(-1, 1)"
27
38
  end
28
39
  end
29
40
 
41
+ # Appends a vertical flip transform.
42
+ # @return [Sevgi::Graphics::Element] self
30
43
  def FlipY
31
44
  tap do
32
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(1, -11)"
45
+ attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(1, -1)"
33
46
  end
34
47
  end
35
48
 
49
+ # Appends a six-value SVG matrix transform.
50
+ # @param values [Array<Numeric>] matrix values
51
+ # @return [Sevgi::Graphics::Element] self
52
+ # @raise [Sevgi::ArgumentError] when exactly six values are not supplied
36
53
  def Matrix(*values)
37
54
  tap do
38
55
  ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
39
56
 
40
- next if values.map(&:to_f).all? { it == 0.0 }
41
-
42
57
  attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "matrix(#{values.join(" ")})"
43
58
  end
44
59
  end
45
60
 
61
+ # Appends an SVG rotate transform.
62
+ # @param a [Numeric] angle in degrees
63
+ # @param origin [Array<Numeric>] optional x and y origin
64
+ # @return [Sevgi::Graphics::Element] self
65
+ # @raise [Sevgi::ArgumentError] when origin is not two coordinates
46
66
  def Rotate(a, *origin)
47
67
  tap do
48
68
  if !origin.empty? && origin.size != 2
@@ -55,28 +75,39 @@ module Sevgi
55
75
  end
56
76
  end
57
77
 
58
- def Rotate90(...) = Rotate(90, ...)
59
-
60
- def Rotate09(...) = Rotate(-90, ...)
61
-
78
+ # @overload RotateRight(*origin)
79
+ # Appends a 90-degree SVG rotate transform.
80
+ # @param origin [Array<Numeric>] optional x and y origin
81
+ # @return [Sevgi::Graphics::Element] self
82
+ def RotateRight(...) = Rotate(90, ...)
83
+
84
+ # @overload RotateLeft(*origin)
85
+ # Appends a -90-degree SVG rotate transform.
86
+ # @param origin [Array<Numeric>] optional x and y origin
87
+ # @return [Sevgi::Graphics::Element] self
88
+ def RotateLeft(...) = Rotate(-90, ...)
89
+
90
+ # Appends an SVG scale transform.
91
+ # @param x [Numeric] x scale, or uniform scale when y is nil
92
+ # @param y [Numeric, nil] y scale
93
+ # @return [Sevgi::Graphics::Element] self
62
94
  def Scale(x, y = nil)
63
95
  tap do
64
- next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
65
-
66
96
  attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(#{(y ? [x, y] : [x]).join(", ")})"
67
97
  end
68
98
  end
69
99
 
70
- def Scale!(...)
71
- Scale(...).tap do
72
- attributes[:"vector-effect"] = "non-scaling-stroke" unless attributes[:"vector-effect"]
73
- end
74
- end
75
-
100
+ # Appends a skew transform through an SVG matrix.
101
+ # @param ax [Numeric] x skew angle in degrees
102
+ # @param ay [Numeric] y skew angle in degrees
103
+ # @return [Sevgi::Graphics::Element] self
76
104
  def Skew(ax, ay)
77
105
  Matrix(1.0, ::Math.tan(ay / 180.0 * ::Math::PI), ::Math.tan(ax / 180.0 * ::Math::PI), 1.0, 0.0, 0.0)
78
106
  end
79
107
 
108
+ # Appends an SVG skewX transform.
109
+ # @param a [Numeric] angle in degrees
110
+ # @return [Sevgi::Graphics::Element] self
80
111
  def SkewX(a)
81
112
  tap do
82
113
  next if a.to_f == 0.0
@@ -85,6 +116,9 @@ module Sevgi
85
116
  end
86
117
  end
87
118
 
119
+ # Appends an SVG skewY transform.
120
+ # @param a [Numeric] angle in degrees
121
+ # @return [Sevgi::Graphics::Element] self
88
122
  def SkewY(a)
89
123
  tap do
90
124
  next if a.to_f == 0.0
@@ -93,6 +127,10 @@ module Sevgi
93
127
  end
94
128
  end
95
129
 
130
+ # Appends an SVG translate transform.
131
+ # @param x [Numeric] x translation
132
+ # @param y [Numeric, nil] y translation
133
+ # @return [Sevgi::Graphics::Element] self
96
134
  def Translate(x, y = nil)
97
135
  tap do
98
136
  next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
@@ -3,18 +3,27 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for floating text, comments, and inherited internal attributes.
6
7
  module Underscore
8
+ # Creates a floating content element.
9
+ # @param contents [Array<Object>] text or content objects
10
+ # @return [Sevgi::Graphics::Element] floating element
7
11
  def _(*contents)
8
12
  Element(:_, *contents)
9
13
  end
10
14
 
15
+ # Adds an XML comment.
16
+ # @param comment [String] comment text
17
+ # @return [Sevgi::Graphics::Element] floating comment element
11
18
  def Comment(comment)
12
19
  _(Content.verbatim("<!-- #{comment} -->"))
13
20
  end
14
21
 
22
+ # Merges internal ancestral attributes from the document root to this element.
23
+ # @return [Hash] merged internal attributes
15
24
  def Ancestral
16
25
  {}.tap do |result|
17
- Root.Traverse() { |element| result.merge!(element[:_]) if element.has?(:_) }
26
+ Root().Traverse() { |element| result.merge!(element[:_]) if element.has?(:_) }
18
27
  end
19
28
  end
20
29
  end
@@ -3,19 +3,28 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for SVG standard validation.
6
7
  module Validate
8
+ # Returns text content used as SVG character data.
9
+ # @return [String, nil] joined text content or nil
7
10
  def CData
8
11
  return if !contents || contents.empty?
9
12
 
10
13
  Content.text(contents)
11
14
  end
12
15
 
16
+ # Reports whether a namespace is available on this element or an ancestor.
17
+ # @param name [Symbol, String] namespace suffix without xmlns:
18
+ # @return [Boolean]
13
19
  def NS?(name)
14
20
  self.class.attributes.key?(key = :"xmlns:#{name}") || TraverseUp { Stay(true) if it.has?(key) } || false
15
21
  end
16
22
 
17
23
  require "sevgi/standard"
18
24
 
25
+ # Validates this subtree against the SVG standard metadata.
26
+ # @return [Sevgi::Graphics::Element] self
27
+ # @raise [Sevgi::ValidationError] when SVG validation fails
19
28
  def Validate
20
29
  Traverse do |element|
21
30
  Standard.conform(
@@ -27,7 +36,12 @@ module Sevgi
27
36
  end
28
37
  end
29
38
 
30
- rescue ::LoadError
39
+ rescue ::LoadError => e
40
+ raise unless e.path == "sevgi/standard"
41
+
42
+ # @overload Validate
43
+ # No-op validation fallback when sevgi/standard is unavailable.
44
+ # @return [nil]
31
45
  def Validate(...)
32
46
  end
33
47
  end