sevgi-graphics 0.95.0 → 0.98.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -0
  3. data/README.md +8 -8
  4. data/lib/sevgi/graphics/attribute.rb +160 -37
  5. data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
  6. data/lib/sevgi/graphics/auxiliary/content.rb +54 -34
  7. data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
  8. data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
  9. data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
  10. data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
  11. data/lib/sevgi/graphics/auxiliary.rb +1 -0
  12. data/lib/sevgi/graphics/document/base.rb +6 -2
  13. data/lib/sevgi/graphics/document/default.rb +1 -1
  14. data/lib/sevgi/graphics/document.rb +236 -102
  15. data/lib/sevgi/graphics/element.rb +114 -31
  16. data/lib/sevgi/graphics/mixtures/call.rb +249 -88
  17. data/lib/sevgi/graphics/mixtures/core.rb +59 -20
  18. data/lib/sevgi/graphics/mixtures/duplicate.rb +49 -15
  19. data/lib/sevgi/graphics/mixtures/export.rb +52 -12
  20. data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
  21. data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
  22. data/lib/sevgi/graphics/mixtures/include.rb +26 -8
  23. data/lib/sevgi/graphics/mixtures/inkscape.rb +201 -47
  24. data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
  25. data/lib/sevgi/graphics/mixtures/render.rb +52 -28
  26. data/lib/sevgi/graphics/mixtures/save.rb +79 -35
  27. data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
  28. data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
  29. data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
  30. data/lib/sevgi/graphics/mixtures/underscore.rb +14 -5
  31. data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
  32. data/lib/sevgi/graphics/mixtures/wrappers.rb +62 -23
  33. data/lib/sevgi/graphics/mixtures.rb +15 -13
  34. data/lib/sevgi/graphics/version.rb +1 -1
  35. data/lib/sevgi/graphics.rb +58 -12
  36. metadata +7 -6
@@ -4,6 +4,12 @@ module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
6
  # DSL helpers for SVG transform attributes.
7
+ #
8
+ # Transform calls are appended in call order and return the element, so they can be composed.
9
+ # @example Compose transforms on one element
10
+ # Sevgi::Graphics.SVG(:minimal) do
11
+ # rect(width: 8, height: 4).Translate(12, 6).Rotate(15, 4, 2)
12
+ # end
7
13
  module Transform
8
14
  # Aligns an inner box inside an outer box.
9
15
  # @param position [Symbol, String, nil] alignment name
@@ -11,12 +17,15 @@ module Sevgi
11
17
  # @param outer [#width, #height, nil] outer box
12
18
  # @return [Sevgi::Graphics::Element] self
13
19
  # @raise [Sevgi::ArgumentError] when alignment is unsupported
20
+ # @raise [Sevgi::ArgumentError] when a box dimension is not a finite real number
14
21
  def Align(position, inner:, outer:)
15
22
  return self unless position && inner && outer
16
23
 
17
24
  case position.to_sym
18
25
  when :center
19
- Translate((outer.width - inner.width) / 2.0, (outer.height - inner.height) / 2.0)
26
+ dimensions = [inner.width, inner.height, outer.width, outer.height]
27
+ iw, ih, ow, oh = dimensions.map { Scalar.number(it, context: "alignment", field: :dimension) }
28
+ Translate((ow - iw) / 2.0, (oh - ih) / 2.0)
20
29
  else
21
30
  ArgumentError.("Unsupported alignment: #{position}")
22
31
  end
@@ -26,7 +35,7 @@ module Sevgi
26
35
  # @return [Sevgi::Graphics::Element] self
27
36
  def Flip
28
37
  tap do
29
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(-1, -1)"
38
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, -1)"
30
39
  end
31
40
  end
32
41
 
@@ -34,7 +43,7 @@ module Sevgi
34
43
  # @return [Sevgi::Graphics::Element] self
35
44
  def FlipX
36
45
  tap do
37
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(-1, 1)"
46
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, 1)"
38
47
  end
39
48
  end
40
49
 
@@ -42,36 +51,40 @@ module Sevgi
42
51
  # @return [Sevgi::Graphics::Element] self
43
52
  def FlipY
44
53
  tap do
45
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(1, -1)"
54
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(1, -1)"
46
55
  end
47
56
  end
48
57
 
49
58
  # Appends a six-value SVG matrix transform.
50
- # @param values [Array<Numeric>] matrix values
59
+ # @param values [Array<Numeric>] finite matrix values, normalized to SVG numbers
51
60
  # @return [Sevgi::Graphics::Element] self
52
- # @raise [Sevgi::ArgumentError] when exactly six values are not supplied
61
+ # @raise [Sevgi::ArgumentError] when six finite real values are not supplied
53
62
  def Matrix(*values)
54
63
  tap do
55
64
  ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
65
+ values = Scalar.numbers(values, context: "matrix")
56
66
 
57
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "matrix(#{values.join(" ")})"
67
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "matrix(#{values.join(" ")})"
58
68
  end
59
69
  end
60
70
 
61
71
  # Appends an SVG rotate transform.
62
- # @param a [Numeric] angle in degrees
63
- # @param origin [Array<Numeric>] optional x and y origin
72
+ # @param a [Numeric] finite angle in degrees, normalized to an SVG number
73
+ # @param origin [Array<Numeric>] optional finite x and y origin, normalized to SVG numbers
64
74
  # @return [Sevgi::Graphics::Element] self
65
- # @raise [Sevgi::ArgumentError] when origin is not two coordinates
75
+ # @raise [Sevgi::ArgumentError] when angle or origin is not finite real, or origin is not two coordinates
66
76
  def Rotate(a, *origin)
67
77
  tap do
68
78
  if !origin.empty? && origin.size != 2
69
79
  ArgumentError.("Incorrect origin (two coordinates required): #{origin}")
70
80
  end
71
81
 
72
- next if a.to_f == 0.0
82
+ angle = Scalar.number(a, context: "rotation", field: :angle)
83
+ origin = Scalar.numbers(origin, context: "rotation")
73
84
 
74
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "rotate(#{[a, *origin].join(", ")})"
85
+ next if angle.zero?
86
+
87
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "rotate(#{[angle, *origin].join(", ")})"
75
88
  end
76
89
  end
77
90
 
@@ -88,56 +101,83 @@ module Sevgi
88
101
  def RotateLeft(...) = Rotate(-90, ...)
89
102
 
90
103
  # 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
104
+ # @param x [Numeric] finite x scale, or uniform scale when y is nil
105
+ # @param y [Numeric, nil] finite y scale
93
106
  # @return [Sevgi::Graphics::Element] self
107
+ # @raise [Sevgi::ArgumentError] when a scale is not a finite real number
94
108
  def Scale(x, y = nil)
95
109
  tap do
96
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "scale(#{(y ? [x, y] : [x]).join(", ")})"
110
+ x = Scalar.number(x, context: "scale", field: :x)
111
+ y = Scalar.number(y, context: "scale", field: :y) unless y.nil?
112
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(#{(y.nil? ? [x] : [x, y]).join(", ")})"
97
113
  end
98
114
  end
99
115
 
100
116
  # 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
117
+ # @param ax [Numeric] finite x skew angle in degrees, normalized before calculation
118
+ # @param ay [Numeric] finite y skew angle in degrees, normalized before calculation
103
119
  # @return [Sevgi::Graphics::Element] self
120
+ # @raise [Sevgi::ArgumentError] when an angle is not a finite real number
104
121
  def Skew(ax, ay)
122
+ ax = Scalar.number(ax, context: "skew", field: :x)
123
+ ay = Scalar.number(ay, context: "skew", field: :y)
105
124
  Matrix(1.0, ::Math.tan(ay / 180.0 * ::Math::PI), ::Math.tan(ax / 180.0 * ::Math::PI), 1.0, 0.0, 0.0)
106
125
  end
107
126
 
108
127
  # Appends an SVG skewX transform.
109
- # @param a [Numeric] angle in degrees
128
+ # @param a [Numeric] finite angle in degrees, normalized to an SVG number
110
129
  # @return [Sevgi::Graphics::Element] self
130
+ # @raise [Sevgi::ArgumentError] when angle is not a finite real number
111
131
  def SkewX(a)
112
132
  tap do
113
- next if a.to_f == 0.0
133
+ angle = Scalar.number(a, context: "skew", field: :x)
134
+ next if angle.zero?
114
135
 
115
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "skewX(#{a})"
136
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewX(#{angle})"
116
137
  end
117
138
  end
118
139
 
119
140
  # Appends an SVG skewY transform.
120
- # @param a [Numeric] angle in degrees
141
+ # @param a [Numeric] finite angle in degrees, normalized to an SVG number
121
142
  # @return [Sevgi::Graphics::Element] self
143
+ # @raise [Sevgi::ArgumentError] when angle is not a finite real number
122
144
  def SkewY(a)
123
145
  tap do
124
- next if a.to_f == 0.0
146
+ angle = Scalar.number(a, context: "skew", field: :y)
147
+ next if angle.zero?
125
148
 
126
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "skewY(#{a})"
149
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewY(#{angle})"
127
150
  end
128
151
  end
129
152
 
130
- # Appends an SVG translate transform.
131
- # @param x [Numeric] x translation
132
- # @param y [Numeric, nil] y translation
153
+ # Appends an SVG translate transform. One argument translates only the x axis.
154
+ # @param x [Numeric] finite x translation, normalized to an SVG number
155
+ # @param y [Numeric, nil] finite y translation, normalized to an SVG number
133
156
  # @return [Sevgi::Graphics::Element] self
157
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
134
158
  def Translate(x, y = nil)
135
159
  tap do
136
- next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
160
+ dx = Scalar.number(x, context: "translation", field: :x)
161
+ dy = Scalar.number(y, context: "translation", field: :y) unless y.nil?
162
+ next if dx.zero? && (dy.nil? || dy.zero?)
137
163
 
138
- attributes[:"transform#{ATTRIBUTE_UPDATE_SUFFIX}"] = "translate(#{(y ? [x, y] : [x]).join(" ")})"
164
+ attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "translate(#{(dy.nil? ? [dx] : [dx, dy]).join(" ")})"
139
165
  end
140
166
  end
167
+
168
+ # Appends an x-axis SVG translate transform.
169
+ # @param x [Numeric] finite x translation
170
+ # @return [Sevgi::Graphics::Element] self
171
+ # @raise [Sevgi::ArgumentError] when x is not a finite real number
172
+ # @see #Translate
173
+ def TranslateX(x) = Translate(x)
174
+
175
+ # Appends a y-axis SVG translate transform.
176
+ # @param y [Numeric] finite y translation
177
+ # @return [Sevgi::Graphics::Element] self
178
+ # @raise [Sevgi::ArgumentError] when y is not a finite real number
179
+ # @see #Translate
180
+ def TranslateY(y) = Translate(0, y)
141
181
  end
142
182
  end
143
183
  end
@@ -3,8 +3,11 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
- # DSL helpers for floating text, comments, and inherited internal attributes.
6
+ # DSL helpers for floating text, comments, and inherited non-rendering context.
7
7
  module Underscore
8
+ CONTEXT = :"#{Attributes::META_PREFIX}context"
9
+ private_constant :CONTEXT
10
+
8
11
  # Creates a floating content element.
9
12
  # @param contents [Array<Object>] text or content objects
10
13
  # @return [Sevgi::Graphics::Element] floating element
@@ -25,16 +28,22 @@ module Sevgi
25
28
  _(Content.verbatim("<!-- #{comment} -->"))
26
29
  end
27
30
 
28
- # Merges internal attributes from the document root, ancestors, and this element.
31
+ # Merges `-context` metadata from the document root, ancestors, and this element.
29
32
  # Only the direct root-to-self ancestor chain participates; sibling subtrees are ignored. When the same key is
30
- # present on multiple chain elements, the nearest element to the receiver wins.
31
- # @return [Hash] merged internal attributes
33
+ # present on multiple chain elements, the nearest element to the receiver wins. Context remains available in
34
+ # memory but is omitted from rendered SVG. It is unrelated to the `_:` XML namespace syntax preserved by
35
+ # Derender.
36
+ # @return [Hash] merged context
37
+ # @example Inherit drawing context without rendering it
38
+ # Sevgi::Graphics.SVG "-context": {tone: "tomato"} do
39
+ # rect fill: Ancestral()[:tone]
40
+ # end
32
41
  def Ancestral
33
42
  chain = []
34
43
  TraverseUp { |element| chain.unshift(element) }
35
44
 
36
45
  {}.tap do |result|
37
- chain.each { |element| result.merge!(element[:_]) if element.has?(:_) }
46
+ chain.each { |element| result.merge!(element[CONTEXT]) if element.has?(CONTEXT) }
38
47
  end
39
48
  end
40
49
  end
@@ -22,7 +22,7 @@ module Sevgi
22
22
  def CData
23
23
  return if !contents || contents.empty?
24
24
 
25
- Content.text(contents)
25
+ contents.join("\n")
26
26
  end
27
27
 
28
28
  # Reports whether a namespace is available on this element or an ancestor.
@@ -41,7 +41,7 @@ module Sevgi
41
41
  Traverse do |element|
42
42
  Standard.conform(
43
43
  element.name,
44
- attributes: element.attributes.list,
44
+ attributes: element.attributes.keys,
45
45
  cdata: element.CData(),
46
46
  elements: element.children.map(&:name)
47
47
  )
@@ -6,21 +6,34 @@ module Sevgi
6
6
  # DSL wrappers for common SVG shapes and content patterns.
7
7
  module Wrappers
8
8
  # Builds a line path ending at an absolute point.
9
- # @param x2 [Numeric] ending x coordinate
10
- # @param y2 [Numeric] ending y coordinate
11
- # @param x1 [Numeric] starting x coordinate
12
- # @param y1 [Numeric] starting y coordinate
9
+ # @example Render a rational coordinate as an SVG number
10
+ # Sevgi::Graphics.SVG { LineTo(x2: Rational(1, 2), y2: 1) }
11
+ # @param x2 [Numeric] finite ending x coordinate
12
+ # @param y2 [Numeric] finite ending y coordinate
13
+ # @param x1 [Numeric] finite starting x coordinate
14
+ # @param y1 [Numeric] finite starting y coordinate
13
15
  # @return [Sevgi::Graphics::Element] path element
16
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
14
17
  def LineTo(x2:, y2:, x1: 0, y1: 0, **)
18
+ x1 = Scalar.number(x1, context: "absolute line", field: :x1)
19
+ y1 = Scalar.number(y1, context: "absolute line", field: :y1)
20
+ x2 = Scalar.number(x2, context: "absolute line", field: :x2)
21
+ y2 = Scalar.number(y2, context: "absolute line", field: :y2)
22
+
15
23
  path(d: "M #{x1} #{y1} L #{x2} #{y2}", **)
16
24
  end
17
25
 
18
26
  # Builds a horizontal line path ending at an absolute x coordinate.
19
- # @param x2 [Numeric] ending x coordinate
20
- # @param x1 [Numeric] starting x coordinate
21
- # @param y1 [Numeric] starting y coordinate
27
+ # @param x2 [Numeric] finite ending x coordinate
28
+ # @param x1 [Numeric] finite starting x coordinate
29
+ # @param y1 [Numeric] finite starting y coordinate
22
30
  # @return [Sevgi::Graphics::Element] path element
31
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
23
32
  def HLineTo(x2:, x1: 0, y1: 0, **)
33
+ x1 = Scalar.number(x1, context: "horizontal line", field: :x1)
34
+ y1 = Scalar.number(y1, context: "horizontal line", field: :y1)
35
+ x2 = Scalar.number(x2, context: "horizontal line", field: :x2)
36
+
24
37
  path(d: "M #{x1} #{y1} H #{x2}", **)
25
38
  end
26
39
 
@@ -41,23 +54,36 @@ module Sevgi
41
54
  end
42
55
 
43
56
  # Builds a vertical line path ending at an absolute y coordinate.
44
- # @param y2 [Numeric] ending y coordinate
45
- # @param x1 [Numeric] starting x coordinate
46
- # @param y1 [Numeric] starting y coordinate
57
+ # @param y2 [Numeric] finite ending y coordinate
58
+ # @param x1 [Numeric] finite starting x coordinate
59
+ # @param y1 [Numeric] finite starting y coordinate
47
60
  # @return [Sevgi::Graphics::Element] path element
61
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
48
62
  def VLineTo(y2:, x1: 0, y1: 0, **)
63
+ x1 = Scalar.number(x1, context: "vertical line", field: :x1)
64
+ y1 = Scalar.number(y1, context: "vertical line", field: :y1)
65
+ y2 = Scalar.number(y2, context: "vertical line", field: :y2)
66
+
49
67
  path(d: "M #{x1} #{y1} V #{y2}", **)
50
68
  end
51
69
 
52
70
  # Builds a relative line path from angle and length.
53
- # @param angle [Numeric] angle in degrees
54
- # @param length [Numeric] line length
55
- # @param x [Numeric] starting x coordinate
56
- # @param y [Numeric] starting y coordinate
71
+ # @param angle [Numeric] finite angle in degrees, normalized before calculation
72
+ # @param length [Numeric] finite line length, normalized before calculation
73
+ # @param x [Numeric] finite starting x coordinate, normalized to an SVG number
74
+ # @param y [Numeric] finite starting y coordinate, normalized to an SVG number
57
75
  # @return [Sevgi::Graphics::Element] path element
76
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
58
77
  def LineBy(angle:, length:, x: 0, y: 0, **)
59
- dx = length * ::Math.cos(angle.to_f / 180 * ::Math::PI)
60
- dy = length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
78
+ angle, length, x, y = Scalar.numbers([angle, length, x, y], context: "relative line")
79
+ dx, dy = Scalar.numbers(
80
+ [
81
+ length * ::Math.cos(angle.to_f / 180 * ::Math::PI),
82
+ length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
83
+ ],
84
+ context: "relative line result"
85
+ )
86
+
61
87
  path(d: "M #{x} #{y} l #{dx} #{dy}", **)
62
88
  end
63
89
 
@@ -74,27 +100,40 @@ module Sevgi
74
100
  end
75
101
 
76
102
  # Builds a relative horizontal line path.
77
- # @param length [Numeric] line length
78
- # @param x [Numeric] starting x coordinate
79
- # @param y [Numeric] starting y coordinate
103
+ # @param length [Numeric] finite line length
104
+ # @param x [Numeric] finite starting x coordinate
105
+ # @param y [Numeric] finite starting y coordinate
80
106
  # @return [Sevgi::Graphics::Element] path element
107
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
81
108
  def HLineBy(length:, x: 0, y: 0, **)
109
+ length = Scalar.number(length, context: "horizontal line", field: :length)
110
+ x = Scalar.number(x, context: "horizontal line", field: :x)
111
+ y = Scalar.number(y, context: "horizontal line", field: :y)
112
+
82
113
  path(d: "M #{x} #{y} h #{length}", **)
83
114
  end
84
115
 
85
116
  # Builds a square rect.
86
- # @param length [Numeric] side length
117
+ # @param length [Numeric] finite side length
87
118
  # @return [Sevgi::Graphics::Element] rect element
119
+ # @raise [Sevgi::ArgumentError] when length is not a finite real number
88
120
  def square(length:, **)
121
+ length = Scalar.number(length, context: "square", field: :length)
122
+
89
123
  rect(width: length, height: length, **)
90
124
  end
91
125
 
92
126
  # Builds a relative vertical line path.
93
- # @param length [Numeric] line length
94
- # @param x [Numeric] starting x coordinate
95
- # @param y [Numeric] starting y coordinate
127
+ # @param length [Numeric] finite line length
128
+ # @param x [Numeric] finite starting x coordinate
129
+ # @param y [Numeric] finite starting y coordinate
96
130
  # @return [Sevgi::Graphics::Element] path element
131
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
97
132
  def VLineBy(length:, x: 0, y: 0, **)
133
+ length = Scalar.number(length, context: "vertical line", field: :length)
134
+ x = Scalar.number(x, context: "vertical line", field: :x)
135
+ y = Scalar.number(y, context: "vertical line", field: :y)
136
+
98
137
  path(d: "M #{x} #{y} v #{length}", **)
99
138
  end
100
139
  end
@@ -23,26 +23,28 @@ module Sevgi
23
23
  mod, document = normalize(mod, document, block)
24
24
  ArgumentError.("Mixture name or block required") if mod == Undefined && !block
25
25
 
26
- document.mixture(mod) unless mod == Undefined
26
+ document.send(:mixture, mod) unless mod == Undefined
27
27
  include_anonymous(document, &block) if block
28
28
  end
29
29
 
30
- def self.anonymous_document?(mod, block)
31
- block && mod.is_a?(::Class) && mod <= Graphics::Document::Proto
32
- end
30
+ class << self
31
+ private
33
32
 
34
- def self.include_anonymous(document, &block)
35
- ::Module.new(&block).tap do |anonymous|
36
- document.send(:include, anonymous)
37
- document.extend(anonymous.const_get(:ClassMethods)) if anonymous.const_defined?(:ClassMethods, false)
33
+ def anonymous_document?(mod, block)
34
+ block && mod.is_a?(::Class) && mod <= Graphics::Document::Proto
38
35
  end
39
- end
40
36
 
41
- def self.normalize(mod, document, block)
42
- anonymous_document?(mod, block) ? [Undefined, mod] : [mod, document]
43
- end
37
+ def include_anonymous(document, &block)
38
+ ::Module.new(&block).tap do |anonymous|
39
+ document.send(:include, anonymous)
40
+ document.extend(anonymous.const_get(:ClassMethods)) if anonymous.const_defined?(:ClassMethods, false)
41
+ end
42
+ end
44
43
 
45
- private_class_method :anonymous_document?, :include_anonymous, :normalize
44
+ def normalize(mod, document, block)
45
+ anonymous_document?(mod, block) ? [Undefined, mod] : [mod, document]
46
+ end
47
+ end
46
48
  end
47
49
  end
48
50
  end
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  # Current graphics component version.
6
- VERSION = "0.95.0"
6
+ VERSION = "0.98.2"
7
7
  end
8
8
  end
@@ -14,15 +14,44 @@ require_relative "graphics/version"
14
14
 
15
15
  module Sevgi
16
16
  # SVG document builder and DSL namespace.
17
+ #
18
+ # A document profile controls root metadata and preambles; a {Canvas} controls
19
+ # physical size, margins, viewport, and viewBox. {Graphics.SVG} combines them
20
+ # into an element tree. Library code keeps that tree as an object until it
21
+ # explicitly renders, validates, saves, or exports it.
22
+ #
23
+ # @example Build and render a minimal SVG document
24
+ # drawing = Sevgi::Graphics.SVG :minimal, width: 10, height: 10 do
25
+ # circle cx: 5, cy: 5, r: 4
26
+ # end
27
+ # drawing.Render #=> "<svg width=\"10\" height=\"10\">\n <circle cx=\"5\" cy=\"5\" r=\"4\"/>\n</svg>"
28
+ # @see https://sevgi.roktas.dev/svg/ SVG construction and profile guide
29
+ # @see https://sevgi.roktas.dev/library-mode/ Library composition guide
30
+ # @see https://sevgi.roktas.dev/showcase/ Runnable drawing examples
31
+ # @see Sevgi::Graphics::Document
32
+ # @see Sevgi::Graphics::Canvas
17
33
  module Graphics
18
- # @overload canvas(arg = Undefined, **kwargs)
19
- # Builds a canvas from a paper profile or explicit size.
20
- # @param arg [Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined] paper profile or paper object
21
- # @param kwargs [Hash] canvas keyword arguments
34
+ # @overload canvas(paper, **overrides)
35
+ # Builds a canvas from a paper profile with optional field overrides.
36
+ # @param paper [Sevgi::Graphics::Paper, Symbol, String] paper object or registered profile
37
+ # @param overrides [Hash] canvas field overrides
22
38
  # @return [Sevgi::Graphics::Canvas]
23
- # @raise [Sevgi::ArgumentError] when the paper profile is unknown
39
+ # @raise [Sevgi::ArgumentError] when the paper or an override is invalid
40
+ # @overload canvas(width:, height:, unit: "mm", name: :custom, margins: [])
41
+ # Builds a canvas from an explicit size.
42
+ # @param width [Numeric] canvas width
43
+ # @param height [Numeric] canvas height
44
+ # @param unit [Symbol, String] SVG unit
45
+ # @param name [Symbol, String] paper name
46
+ # @param margins [Array<Numeric>] margin shorthand values
47
+ # @return [Sevgi::Graphics::Canvas]
48
+ # @raise [Sevgi::ArgumentError] when a required field is omitted or a value is invalid
49
+ # @example Use the component constructor in library code
50
+ # page = Sevgi::Graphics.canvas(:a4, margins: [12, 10])
51
+ # icon = Sevgi::Graphics.canvas(width: 24, height: 24, unit: :px)
52
+ # [page.name, icon.viewport] # => [:a4, {width: "24.0px", height: "24.0px"}]
24
53
  def canvas(...)
25
- Graphics::Canvas.from_paper(...)
54
+ Graphics::Canvas.call(...)
26
55
  end
27
56
 
28
57
  # @overload document(name)
@@ -34,8 +63,10 @@ module Sevgi
34
63
  # Looks up a named profile when both definition keywords are omitted. Supplying `preambles:` or `attributes:`
35
64
  # defines a named profile, or returns an existing profile when every explicitly supplied field matches. Omitted
36
65
  # fields are ignored during existing-profile comparison. Profile containers and strings are copied into the
37
- # process-global, thread-atomic registry; mutable non-container attribute values are stringified once before
38
- # registration. Concurrent identical definitions return the same canonical registered class.
66
+ # process-global, thread-atomic registry; attribute names and nested Hash keys are normalized, nil attributes are
67
+ # omitted, update suffixes are retained for document-class inheritance, and mutable non-container values are
68
+ # stringified once before registration. Concurrent identical definitions return the same canonical registered
69
+ # class.
39
70
  # @param name [Symbol, String] profile name
40
71
  # @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
41
72
  # @param attributes [Hash, nil, Sevgi::Undefined] default root attributes; nil means an empty Hash
@@ -48,12 +79,25 @@ module Sevgi
48
79
  # @return [Class] anonymous document class
49
80
  # @raise [Sevgi::ArgumentError] when metadata is invalid XML, cyclic, or cannot be stringified
50
81
  # @return [Class] document class
82
+ # @example Define, look up, and inspect a named document
83
+ # Sevgi::Graphics.document(:card, attributes: {viewBox: "0 0 100 60"})
84
+ # Sevgi::Graphics::Document.fetch(:card) # => the registered document class
85
+ # Sevgi::Graphics::Document.profile(:card).attributes # => {viewBox: "0 0 100 60"}
86
+ # @example Define an anonymous document without changing the registry
87
+ # klass = Sevgi::Graphics.document(attributes: {viewBox: "0 0 10 10"})
88
+ # klass.profile.name # => nil
89
+ # @example Define and use a named profile from library code
90
+ # Sevgi::Graphics.document(:badge, attributes: {viewBox: "0 0 24 24"})
91
+ # drawing = Sevgi::Graphics.SVG(:badge) { circle cx: 12, cy: 12, r: 10 }
92
+ # drawing[:viewBox] # => "0 0 24 24"
51
93
  def document(name = Undefined, preambles: Undefined, attributes: Undefined)
52
94
  Graphics::Document.define(name, preambles:, attributes:)
53
95
  end
54
96
 
55
97
  # Defines or replaces a document profile class.
56
98
  # Validation and snapshot capture complete before an existing registration is atomically replaced.
99
+ # Use the non-bang {#document} for ordinary idempotent definitions; this
100
+ # bang form is an explicit process-global replacement.
57
101
  # @param name [Symbol, String] profile name
58
102
  # @param preambles [Array<String>, nil] document preamble lines
59
103
  # @param attributes [Hash, nil] default root attributes; nil means an empty Hash
@@ -78,6 +122,7 @@ module Sevgi
78
122
  end
79
123
 
80
124
  # Defines or replaces a paper profile.
125
+ # Use this only when replacing the process-global meaning of a name is intentional.
81
126
  # @param width [Numeric] paper width
82
127
  # @param height [Numeric] paper height
83
128
  # @param name [Symbol, String] profile name
@@ -85,10 +130,13 @@ module Sevgi
85
130
  # @return [Symbol, String] original profile name
86
131
  # @raise [Sevgi::ArgumentError] when the profile is invalid or the profile name is reserved
87
132
  def paper!(width, height, name = :custom, unit: "mm")
88
- name.tap { Graphics::Paper.define(name, width:, height:, unit:) }
133
+ name.tap { Graphics::Paper.define(name, width:, height:, unit:, overwrite: true) }
89
134
  end
90
135
 
91
- # Builds an SVG root document.
136
+ # Builds an SVG root element tree without rendering it.
137
+ #
138
+ # The first argument selects root metadata; the second supplies physical
139
+ # canvas attributes. Keyword attributes are applied to the root after both.
92
140
  # @param document [Symbol, String, Class] document profile name or document class
93
141
  # @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
94
142
  # @yield evaluates the drawing DSL in the root element
@@ -102,6 +150,4 @@ module Sevgi
102
150
  extend self
103
151
  end
104
152
 
105
- # Top-level alias for the graphics DSL namespace.
106
- SVG = Graphics
107
153
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-graphics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.95.0
4
+ version: 0.98.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,15 +15,15 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.95.0
18
+ version: 0.98.2
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.95.0
26
- description: Provides an extensible DSL for creating SVG content.
25
+ version: 0.98.2
26
+ description: Builds and renders SVG document trees from Ruby.
27
27
  email: roktas@gmail.com
28
28
  executables: []
29
29
  extensions: []
@@ -39,6 +39,7 @@ files:
39
39
  - lib/sevgi/graphics/auxiliary/content.rb
40
40
  - lib/sevgi/graphics/auxiliary/margin.rb
41
41
  - lib/sevgi/graphics/auxiliary/paper.rb
42
+ - lib/sevgi/graphics/auxiliary/path.rb
42
43
  - lib/sevgi/graphics/auxiliary/scalar.rb
43
44
  - lib/sevgi/graphics/auxiliary/sizes.rb
44
45
  - lib/sevgi/graphics/document.rb
@@ -92,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
93
  - !ruby/object:Gem::Version
93
94
  version: '0'
94
95
  requirements: []
95
- rubygems_version: 4.0.11
96
+ rubygems_version: 4.0.16
96
97
  specification_version: 4
97
- summary: Core DSL for the Sevgi toolkit.
98
+ summary: Core SVG DSL for Sevgi.
98
99
  test_files: []