sevgi-graphics 0.95.0 → 1.0.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -2
  3. data/README.md +12 -9
  4. data/lib/sevgi/graphics/attribute.rb +166 -45
  5. data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
  6. data/lib/sevgi/graphics/auxiliary/content.rb +56 -47
  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 +239 -117
  15. data/lib/sevgi/graphics/element.rb +132 -34
  16. data/lib/sevgi/graphics/mixtures/call.rb +234 -88
  17. data/lib/sevgi/graphics/mixtures/core.rb +67 -27
  18. data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -25
  19. data/lib/sevgi/graphics/mixtures/export.rb +54 -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 +214 -47
  24. data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
  25. data/lib/sevgi/graphics/mixtures/render.rb +60 -120
  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 +89 -30
  30. data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
  31. data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
  32. data/lib/sevgi/graphics/mixtures/wrappers.rb +111 -23
  33. data/lib/sevgi/graphics/mixtures.rb +15 -13
  34. data/lib/sevgi/graphics/version.rb +1 -1
  35. data/lib/sevgi/graphics/xml.rb +4 -9
  36. data/lib/sevgi/graphics.rb +69 -18
  37. metadata +7 -6
@@ -5,22 +5,84 @@ module Sevgi
5
5
  module Mixtures
6
6
  # DSL wrappers for common SVG shapes and content patterns.
7
7
  module Wrappers
8
+ # rubocop:disable Metrics/ParameterLists
9
+
10
+ # Builds an elliptical arc path ending at an absolute point.
11
+ # SVG resolves the ellipse from its endpoints, radii, and flags, including radius correction and zero radii.
12
+ # @example Draw an upper semicircle in screen coordinates
13
+ # Sevgi::Graphics.SVG { ArcTo x1: 0, y1: 10, x2: 20, y2: 10, rx: 10, ry: 10, sweep: true }
14
+ # @param x2 [Numeric] finite ending x coordinate
15
+ # @param y2 [Numeric] finite ending y coordinate
16
+ # @param rx [Numeric] finite local x radius
17
+ # @param ry [Numeric] finite local y radius
18
+ # @param x1 [Numeric] finite starting x coordinate
19
+ # @param y1 [Numeric] finite starting y coordinate
20
+ # @param rotation [Numeric] clockwise ellipse rotation in degrees
21
+ # @param large [Boolean] select the arc exceeding a half turn
22
+ # @param sweep [Boolean] select increasing parameter angles
23
+ # @return [Sevgi::Graphics::Element] path element
24
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or a flag is not Boolean
25
+ def ArcTo(x2:, y2:, rx:, ry:, x1: 0, y1: 0, rotation: 0, large: false, sweep: false, **)
26
+ x1, y1, x2, y2, rx, ry, rotation = Scalar.numbers([x1, y1, x2, y2, rx, ry, rotation], context: "absolute arc")
27
+ unless [large, sweep].all? { it.equal?(true) || it.equal?(false) }
28
+ ArgumentError.("Arc flags must be Boolean")
29
+ end
30
+
31
+ path(d: "M #{x1} #{y1} A #{rx} #{ry} #{rotation} #{large ? 1 : 0} #{sweep ? 1 : 0} #{x2} #{y2}", **)
32
+ end
33
+
34
+ # Builds an elliptical arc path ending at a relative offset.
35
+ # @param dx [Numeric] finite x displacement from the starting point
36
+ # @param dy [Numeric] finite y displacement from the starting point
37
+ # @param rx [Numeric] finite local x radius
38
+ # @param ry [Numeric] finite local y radius
39
+ # @param x [Numeric] finite starting x coordinate
40
+ # @param y [Numeric] finite starting y coordinate
41
+ # @param rotation [Numeric] clockwise ellipse rotation in degrees
42
+ # @param large [Boolean] select the arc exceeding a half turn
43
+ # @param sweep [Boolean] select increasing parameter angles
44
+ # @return [Sevgi::Graphics::Element] path element
45
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or a flag is not Boolean
46
+ # @see #ArcTo
47
+ def ArcBy(dx:, dy:, rx:, ry:, x: 0, y: 0, rotation: 0, large: false, sweep: false, **)
48
+ x, y, dx, dy, rx, ry, rotation = Scalar.numbers([x, y, dx, dy, rx, ry, rotation], context: "relative arc")
49
+ unless [large, sweep].all? { it.equal?(true) || it.equal?(false) }
50
+ ArgumentError.("Arc flags must be Boolean")
51
+ end
52
+
53
+ path(d: "M #{x} #{y} a #{rx} #{ry} #{rotation} #{large ? 1 : 0} #{sweep ? 1 : 0} #{dx} #{dy}", **)
54
+ end
55
+ # rubocop:enable Metrics/ParameterLists
56
+
8
57
  # 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
58
+ # @example Render a rational coordinate as an SVG number
59
+ # Sevgi::Graphics.SVG { LineTo(x2: Rational(1, 2), y2: 1) }
60
+ # @param x2 [Numeric] finite ending x coordinate
61
+ # @param y2 [Numeric] finite ending y coordinate
62
+ # @param x1 [Numeric] finite starting x coordinate
63
+ # @param y1 [Numeric] finite starting y coordinate
13
64
  # @return [Sevgi::Graphics::Element] path element
65
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
14
66
  def LineTo(x2:, y2:, x1: 0, y1: 0, **)
67
+ x1 = Scalar.number(x1, context: "absolute line", field: :x1)
68
+ y1 = Scalar.number(y1, context: "absolute line", field: :y1)
69
+ x2 = Scalar.number(x2, context: "absolute line", field: :x2)
70
+ y2 = Scalar.number(y2, context: "absolute line", field: :y2)
71
+
15
72
  path(d: "M #{x1} #{y1} L #{x2} #{y2}", **)
16
73
  end
17
74
 
18
75
  # 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
76
+ # @param x2 [Numeric] finite ending x coordinate
77
+ # @param x1 [Numeric] finite starting x coordinate
78
+ # @param y1 [Numeric] finite starting y coordinate
22
79
  # @return [Sevgi::Graphics::Element] path element
80
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
23
81
  def HLineTo(x2:, x1: 0, y1: 0, **)
82
+ x1 = Scalar.number(x1, context: "horizontal line", field: :x1)
83
+ y1 = Scalar.number(y1, context: "horizontal line", field: :y1)
84
+ x2 = Scalar.number(x2, context: "horizontal line", field: :x2)
85
+
24
86
  path(d: "M #{x1} #{y1} H #{x2}", **)
25
87
  end
26
88
 
@@ -41,23 +103,36 @@ module Sevgi
41
103
  end
42
104
 
43
105
  # 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
106
+ # @param y2 [Numeric] finite ending y coordinate
107
+ # @param x1 [Numeric] finite starting x coordinate
108
+ # @param y1 [Numeric] finite starting y coordinate
47
109
  # @return [Sevgi::Graphics::Element] path element
110
+ # @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
48
111
  def VLineTo(y2:, x1: 0, y1: 0, **)
112
+ x1 = Scalar.number(x1, context: "vertical line", field: :x1)
113
+ y1 = Scalar.number(y1, context: "vertical line", field: :y1)
114
+ y2 = Scalar.number(y2, context: "vertical line", field: :y2)
115
+
49
116
  path(d: "M #{x1} #{y1} V #{y2}", **)
50
117
  end
51
118
 
52
119
  # 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
120
+ # @param angle [Numeric] finite angle in degrees, normalized before calculation
121
+ # @param length [Numeric] finite line length, normalized before calculation
122
+ # @param x [Numeric] finite starting x coordinate, normalized to an SVG number
123
+ # @param y [Numeric] finite starting y coordinate, normalized to an SVG number
57
124
  # @return [Sevgi::Graphics::Element] path element
125
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
58
126
  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)
127
+ angle, length, x, y = Scalar.numbers([angle, length, x, y], context: "relative line")
128
+ dx, dy = Scalar.numbers(
129
+ [
130
+ length * ::Math.cos(angle.to_f / 180 * ::Math::PI),
131
+ length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
132
+ ],
133
+ context: "relative line result"
134
+ )
135
+
61
136
  path(d: "M #{x} #{y} l #{dx} #{dy}", **)
62
137
  end
63
138
 
@@ -74,27 +149,40 @@ module Sevgi
74
149
  end
75
150
 
76
151
  # 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
152
+ # @param length [Numeric] finite line length
153
+ # @param x [Numeric] finite starting x coordinate
154
+ # @param y [Numeric] finite starting y coordinate
80
155
  # @return [Sevgi::Graphics::Element] path element
156
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
81
157
  def HLineBy(length:, x: 0, y: 0, **)
158
+ length = Scalar.number(length, context: "horizontal line", field: :length)
159
+ x = Scalar.number(x, context: "horizontal line", field: :x)
160
+ y = Scalar.number(y, context: "horizontal line", field: :y)
161
+
82
162
  path(d: "M #{x} #{y} h #{length}", **)
83
163
  end
84
164
 
85
165
  # Builds a square rect.
86
- # @param length [Numeric] side length
166
+ # @param length [Numeric] finite side length
87
167
  # @return [Sevgi::Graphics::Element] rect element
168
+ # @raise [Sevgi::ArgumentError] when length is not a finite real number
88
169
  def square(length:, **)
170
+ length = Scalar.number(length, context: "square", field: :length)
171
+
89
172
  rect(width: length, height: length, **)
90
173
  end
91
174
 
92
175
  # 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
176
+ # @param length [Numeric] finite line length
177
+ # @param x [Numeric] finite starting x coordinate
178
+ # @param y [Numeric] finite starting y coordinate
96
179
  # @return [Sevgi::Graphics::Element] path element
180
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
97
181
  def VLineBy(length:, x: 0, y: 0, **)
182
+ length = Scalar.number(length, context: "vertical line", field: :length)
183
+ x = Scalar.number(x, context: "vertical line", field: :x)
184
+ y = Scalar.number(y, context: "vertical line", field: :y)
185
+
98
186
  path(d: "M #{x} #{y} v #{length}", **)
99
187
  end
100
188
  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 = "1.0.0"
7
7
  end
8
8
  end
@@ -11,8 +11,9 @@ module Sevgi
11
11
  NCNAME_CHAR = "#{NCNAME_START}\\-.0-9\u{B7}\u{300}-\u{36F}\u{203F}-\u{2040}".freeze
12
12
  NCNAME = "[#{NCNAME_START}][#{NCNAME_CHAR}]*".freeze
13
13
  QNAME = /\A#{NCNAME}(?::#{NCNAME})?\z/u
14
+ ILLEGAL_CHARACTER = /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/u
14
15
 
15
- private_constant :NCNAME, :NCNAME_CHAR, :NCNAME_START, :QNAME
16
+ private_constant :NCNAME, :NCNAME_CHAR, :NCNAME_START, :QNAME, :ILLEGAL_CHARACTER
16
17
 
17
18
  class << self
18
19
  # Validates an XML 1.0 string representation.
@@ -104,8 +105,8 @@ module Sevgi
104
105
  ArgumentError.("#{context} must be valid UTF-8") unless value.valid_encoding?
105
106
 
106
107
  text = value.encode("UTF-8")
107
- if (codepoint = text.each_codepoint.find { !legal_codepoint?(it) })
108
- ArgumentError.("#{context} contains illegal character U+#{format("%04X", codepoint)}")
108
+ if (match = ILLEGAL_CHARACTER.match(text))
109
+ ArgumentError.("#{context} contains illegal character U+#{format("%04X", match[0].ord)}")
109
110
  end
110
111
 
111
112
  text
@@ -113,12 +114,6 @@ module Sevgi
113
114
  ArgumentError.("#{context} must be valid UTF-8: #{e.message}")
114
115
  end
115
116
 
116
- def legal_codepoint?(codepoint)
117
- [0x9, 0xA, 0xD].include?(codepoint) ||
118
- (0x20..0xD7FF).cover?(codepoint) ||
119
- (0xE000..0xFFFD).cover?(codepoint) ||
120
- (0x10000..0x10FFFF).cover?(codepoint)
121
- end
122
117
  end
123
118
  end
124
119
 
@@ -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/documents/ SVG construction and profile guide
29
+ # @see https://sevgi.roktas.dev/compose/ Composition guide
30
+ # @see https://sevgi.roktas.dev/examples/ 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,37 +63,52 @@ 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 remain available for document-class inheritance. Mutable non-container values are
68
+ # stringified once before registration. Concurrent identical definitions return the same registered class.
39
69
  # @param name [Symbol, String] profile name
40
70
  # @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
41
- # @param attributes [Hash, nil, Sevgi::Undefined] default root attributes; nil means an empty Hash
71
+ # @param attributes [Hash, nil, Sevgi::Undefined] default root attributes. Nil means an empty Hash
42
72
  # @return [Class] document class
43
73
  # @raise [Sevgi::ArgumentError] when a profile conflicts or metadata is invalid XML, cyclic, or cannot be stringified
44
74
  # @overload document(preambles: Undefined, attributes: Undefined)
45
75
  # Defines an anonymous document profile without registering it globally.
46
76
  # @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
47
- # @param attributes [Hash, nil, Sevgi::Undefined] default root attributes; nil means an empty Hash
77
+ # @param attributes [Hash, nil, Sevgi::Undefined] default root attributes. Nil means an empty Hash
48
78
  # @return [Class] anonymous document class
49
79
  # @raise [Sevgi::ArgumentError] when metadata is invalid XML, cyclic, or cannot be stringified
50
80
  # @return [Class] document class
81
+ # @example Define, look up, and inspect a named document
82
+ # Sevgi::Graphics.document(:card, attributes: {viewBox: "0 0 100 60"})
83
+ # Sevgi::Graphics::Document.fetch(:card) # => the registered document class
84
+ # Sevgi::Graphics::Document.profile(:card).attributes # => {viewBox: "0 0 100 60"}
85
+ # @example Define an anonymous document without changing the registry
86
+ # klass = Sevgi::Graphics.document(attributes: {viewBox: "0 0 10 10"})
87
+ # klass.profile.name # => nil
88
+ # @example Define and use a named profile from library code
89
+ # Sevgi::Graphics.document(:badge, attributes: {viewBox: "0 0 24 24"})
90
+ # drawing = Sevgi::Graphics.SVG(:badge) { circle cx: 12, cy: 12, r: 10 }
91
+ # drawing[:viewBox] # => "0 0 24 24"
51
92
  def document(name = Undefined, preambles: Undefined, attributes: Undefined)
52
93
  Graphics::Document.define(name, preambles:, attributes:)
53
94
  end
54
95
 
55
96
  # Defines or replaces a document profile class.
56
97
  # Validation and snapshot capture complete before an existing registration is atomically replaced.
98
+ # Use the non-bang {#document} for ordinary idempotent definitions. This
99
+ # bang form is an explicit process-global replacement.
57
100
  # @param name [Symbol, String] profile name
58
101
  # @param preambles [Array<String>, nil] document preamble lines
59
- # @param attributes [Hash, nil] default root attributes; nil means an empty Hash
102
+ # @param attributes [Hash, nil] default root attributes. Nil means an empty Hash
60
103
  # @return [Class] document class
61
104
  # @raise [Sevgi::ArgumentError] when the name or metadata is invalid XML, cyclic, or cannot be stringified
62
105
  def document!(name, preambles: [], attributes: {})
63
106
  Graphics::Document.define(name, preambles:, attributes:, overwrite: true)
64
107
  end
65
108
 
66
- # Defines a paper profile unless the same profile already exists. Registration is process-global and thread-atomic;
67
- # an identical concurrent definition is idempotent and a conflicting definition is rejected.
109
+ # Defines a paper profile unless the same profile already exists.
110
+ # Registration is process-global and thread-atomic. Identical concurrent definitions are idempotent. Conflicting
111
+ # definitions are rejected.
68
112
  # @param width [Numeric] paper width
69
113
  # @param height [Numeric] paper height
70
114
  # @param name [Symbol, String] profile name
@@ -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,23 +130,29 @@ 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.
92
- # @param document [Symbol, String, Class] document profile name or document class
136
+ # Builds an SVG root element tree without rendering it.
137
+ #
138
+ # The first argument selects root metadata, or supplies the canvas while using the default profile. The second
139
+ # supplies canvas attributes when the first argument is a profile. Keyword attributes are applied after both.
140
+ # @param document [Symbol, String, Class, Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper] document profile,
141
+ # document class, or canvas input that uses the default profile
93
142
  # @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
94
143
  # @yield evaluates the drawing DSL in the root element
95
144
  # @yieldreturn [Object] ignored block result
96
145
  # @return [Sevgi::Graphics::Document::Proto] SVG root element
97
146
  # @raise [Sevgi::ArgumentError] when the document/canvas profile or root XML attributes are invalid
98
147
  def SVG(document = :default, canvas = Undefined, **, &block)
148
+ if canvas.equal?(Undefined) && (document.is_a?(Canvas) || document.is_a?(Paper))
149
+ document, canvas = :default, document
150
+ end
151
+
99
152
  Graphics::Document.(document, canvas, **, &block)
100
153
  end
101
154
 
102
155
  extend self
103
156
  end
104
157
 
105
- # Top-level alias for the graphics DSL namespace.
106
- SVG = Graphics
107
158
  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: 1.0.0
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: 1.0.0
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: 1.0.0
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.20
96
97
  specification_version: 4
97
- summary: Core DSL for the Sevgi toolkit.
98
+ summary: Core SVG DSL for Sevgi.
98
99
  test_files: []