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
@@ -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,38 @@
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 [Object] comment text
17
+ # @return [Sevgi::Graphics::Element] floating comment element
18
+ # @raise [Sevgi::ArgumentError] when comment text would produce malformed XML
11
19
  def Comment(comment)
20
+ comment = comment.to_s
21
+
22
+ ArgumentError.("XML comment must not contain '--'") if comment.include?("--")
23
+ ArgumentError.("XML comment must not end with '-'") if comment.end_with?("-")
24
+
12
25
  _(Content.verbatim("<!-- #{comment} -->"))
13
26
  end
14
27
 
28
+ # Merges internal attributes from the document root, ancestors, and this element.
29
+ # 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
15
32
  def Ancestral
33
+ chain = []
34
+ TraverseUp { |element| chain.unshift(element) }
35
+
16
36
  {}.tap do |result|
17
- Root.Traverse() { |element| result.merge!(element[:_]) if element.has?(:_) }
37
+ chain.each { |element| result.merge!(element[:_]) if element.has?(:_) }
18
38
  end
19
39
  end
20
40
  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
@@ -3,15 +3,31 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL wrappers for common SVG shapes and content patterns.
6
7
  module Wrappers
7
- def Cline(x2:, y2:, x1: 0, y1: 0, **)
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
13
+ # @return [Sevgi::Graphics::Element] path element
14
+ def LineTo(x2:, y2:, x1: 0, y1: 0, **)
8
15
  path(d: "M #{x1} #{y1} L #{x2} #{y2}", **)
9
16
  end
10
17
 
11
- def Hline(x2:, x1: 0, y1: 0, **)
18
+ # 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
22
+ # @return [Sevgi::Graphics::Element] path element
23
+ def HLineTo(x2:, x1: 0, y1: 0, **)
12
24
  path(d: "M #{x1} #{y1} H #{x2}", **)
13
25
  end
14
26
 
27
+ # Builds a titled SVG symbol.
28
+ # @param name [String] human-readable symbol name
29
+ # @param kwargs [Hash] symbol attributes
30
+ # @return [Sevgi::Graphics::Element] symbol element
15
31
  def Symbol(name, **kwargs, &block)
16
32
  id = (words = name.split).map(&:downcase).join("-")
17
33
  title = words.map(&:capitalize).join(" ")
@@ -22,39 +38,60 @@ module Sevgi
22
38
  end
23
39
  end
24
40
 
25
- def Vline(y2:, x1: 0, y1: 0, **)
41
+ # Builds a vertical line path ending at an absolute y coordinate.
42
+ # @param y2 [Numeric] ending y coordinate
43
+ # @param x1 [Numeric] starting x coordinate
44
+ # @param y1 [Numeric] starting y coordinate
45
+ # @return [Sevgi::Graphics::Element] path element
46
+ def VLineTo(y2:, x1: 0, y1: 0, **)
26
47
  path(d: "M #{x1} #{y1} V #{y2}", **)
27
48
  end
28
49
 
29
- def cline(angle:, length:, x: 0, y: 0, **)
50
+ # Builds a relative line path from angle and length.
51
+ # @param angle [Numeric] angle in degrees
52
+ # @param length [Numeric] line length
53
+ # @param x [Numeric] starting x coordinate
54
+ # @param y [Numeric] starting y coordinate
55
+ # @return [Sevgi::Graphics::Element] path element
56
+ def LineBy(angle:, length:, x: 0, y: 0, **)
30
57
  dx = length * ::Math.cos(angle.to_f / 180 * ::Math::PI)
31
58
  dy = length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
32
59
  path(d: "M #{x} #{y} l #{dx} #{dy}", **)
33
60
  end
34
61
 
35
- def css(hash, **)
36
- style(Content.css(hash), type: "text/css", **)
37
- end
62
+ # Builds a style element with CSS content.
63
+ # @param hash [Hash, nil] CSS rules
64
+ # @param attributes [Hash] style attributes or CSS rules when hash is nil
65
+ # @return [Sevgi::Graphics::Element] style element
66
+ # @raise [Sevgi::ArgumentError] when CSS content is not a hash
67
+ def css(hash = nil, **attributes)
68
+ hash, attributes = attributes, {} unless hash
38
69
 
39
- def cxline(angle:, dx:, x: 0, y: 0, **)
40
- dy = dx * ::Math.tan(angle.to_f / 180 * ::Math::PI)
41
- path(d: "M #{x} #{y} l #{dx} #{dy}", **)
42
- end
43
-
44
- def cyline(angle:, dy:, x: 0, y: 0, **)
45
- dx = dy / ::Math.tan(angle.to_f / 180 * ::Math::PI)
46
- path(d: "M #{x} #{y} l #{dx} #{dy}", **)
70
+ style(Content.css(hash), type: "text/css", **attributes)
47
71
  end
48
72
 
49
- def hline(length:, x: 0, y: 0, **)
73
+ # Builds a relative horizontal line path.
74
+ # @param length [Numeric] line length
75
+ # @param x [Numeric] starting x coordinate
76
+ # @param y [Numeric] starting y coordinate
77
+ # @return [Sevgi::Graphics::Element] path element
78
+ def HLineBy(length:, x: 0, y: 0, **)
50
79
  path(d: "M #{x} #{y} h #{length}", **)
51
80
  end
52
81
 
82
+ # Builds a square rect.
83
+ # @param length [Numeric] side length
84
+ # @return [Sevgi::Graphics::Element] rect element
53
85
  def square(length:, **)
54
86
  rect(width: length, height: length, **)
55
87
  end
56
88
 
57
- def vline(length:, x: 0, y: 0, **)
89
+ # Builds a relative vertical line path.
90
+ # @param length [Numeric] line length
91
+ # @param x [Numeric] starting x coordinate
92
+ # @param y [Numeric] starting y coordinate
93
+ # @return [Sevgi::Graphics::Element] path element
94
+ def VLineBy(length:, x: 0, y: 0, **)
58
95
  path(d: "M #{x} #{y} v #{length}", **)
59
96
  end
60
97
  end
@@ -2,11 +2,43 @@
2
2
 
3
3
  module Sevgi
4
4
  module Graphics
5
+ # Mixins that extend document classes with DSL helper methods.
5
6
  module Mixtures
6
- def self.mixin(mod, document = Graphics::Document::Base, &block)
7
- document.mixture(mod)
8
- document.mixture(::Module.new(&block)) if block
7
+ # @overload mixin(mod, document = Graphics::Document::Base, &block)
8
+ # Includes a named mixture and optional anonymous extension into a document class.
9
+ # @param mod [Symbol, String] mixture constant name
10
+ # @param document [Class] document class receiving the mixture
11
+ # @return [Module, nil] optional anonymous mixture when a block is given
12
+ # @raise [NameError] when the mixture does not exist
13
+ # @overload mixin(document = Graphics::Document::Base, &block)
14
+ # Includes only an anonymous extension into a document class.
15
+ # @param document [Class] document class receiving the mixture
16
+ # @return [Module] anonymous mixture
17
+ # @raise [Sevgi::ArgumentError] when no named mixture or block is given
18
+ def self.mixin(mod = Undefined, document = Graphics::Document::Base, &block)
19
+ mod, document = normalize(mod, document, block)
20
+ ArgumentError.("Mixture name or block required") if mod == Undefined && !block
21
+
22
+ document.mixture(mod) unless mod == Undefined
23
+ include_anonymous(document, &block) if block
24
+ end
25
+
26
+ def self.anonymous_document?(mod, block)
27
+ block && mod.is_a?(::Class) && mod <= Graphics::Document::Proto
28
+ end
29
+
30
+ def self.include_anonymous(document, &block)
31
+ ::Module.new(&block).tap do |anonymous|
32
+ document.send(:include, anonymous)
33
+ document.extend(anonymous.const_get(:ClassMethods)) if anonymous.const_defined?(:ClassMethods, false)
34
+ end
35
+ end
36
+
37
+ def self.normalize(mod, document, block)
38
+ anonymous_document?(mod, block) ? [Undefined, mod] : [mod, document]
9
39
  end
40
+
41
+ private_class_method :anonymous_document?, :include_anonymous, :normalize
10
42
  end
11
43
  end
12
44
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Sevgi
4
4
  module Graphics
5
- VERSION = "0.73.2"
5
+ # Current graphics component version.
6
+ VERSION = "0.94.0"
6
7
  end
7
8
  end
@@ -3,7 +3,7 @@
3
3
  require "sevgi/function"
4
4
 
5
5
  require_relative "graphics/attribute"
6
- require_relative "graphics/auxilary"
6
+ require_relative "graphics/auxiliary"
7
7
  require_relative "graphics/element"
8
8
  require_relative "graphics/mixtures"
9
9
 
@@ -12,23 +12,88 @@ require_relative "graphics/document"
12
12
  require_relative "graphics/version"
13
13
 
14
14
  module Sevgi
15
+ # SVG document builder and DSL namespace.
15
16
  module Graphics
17
+ # @overload canvas(arg = Undefined, **kwargs)
18
+ # Builds a canvas from a paper profile or explicit size.
19
+ # @param arg [Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined] paper profile or paper object
20
+ # @param kwargs [Hash] canvas keyword arguments
21
+ # @return [Sevgi::Graphics::Canvas]
22
+ # @raise [Sevgi::ArgumentError] when the paper profile is unknown
16
23
  def canvas(...)
17
- Graphics::Canvas.(...)
24
+ Graphics::Canvas.from_paper(...)
18
25
  end
19
26
 
20
- def document(name = :default, preambles: [], attributes: {})
21
- Class.new(Graphics::Document::Base) { document(name, preambles:, attributes:) }
27
+ # @overload document(name)
28
+ # Looks up an existing document profile by name.
29
+ # @param name [Symbol, String] profile name
30
+ # @return [Class] document class
31
+ # @raise [Sevgi::ArgumentError] when the profile is unknown
32
+ # @overload document(name, preambles: Undefined, attributes: Undefined)
33
+ # Defines a named document profile, or returns an existing profile when every explicitly supplied field matches.
34
+ # Omitted fields are ignored during existing-profile comparison. Profile inputs are copied into the registry.
35
+ # @param name [Symbol, String] profile name
36
+ # @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
37
+ # @param attributes [Hash, Sevgi::Undefined] default root attributes
38
+ # @return [Class] document class
39
+ # @raise [Sevgi::ArgumentError] when a named profile conflicts with an existing profile
40
+ # @overload document(preambles: Undefined, attributes: Undefined)
41
+ # Defines an anonymous document profile without registering it globally.
42
+ # @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
43
+ # @param attributes [Hash, Sevgi::Undefined] default root attributes
44
+ # @return [Class] anonymous document class
45
+ # @raise [Sevgi::ArgumentError] when profile input is invalid
46
+ # @return [Class] document class
47
+ def document(name = Undefined, preambles: Undefined, attributes: Undefined)
48
+ Graphics::Document.define(name, preambles:, attributes:)
22
49
  end
23
50
 
51
+ # Defines or replaces a document profile class.
52
+ # @param name [Symbol, String] profile name
53
+ # @param preambles [Array<String>, nil] document preamble lines
54
+ # @param attributes [Hash] default root attributes
55
+ # @return [Class] document class
56
+ # @raise [Sevgi::ArgumentError] when the profile name is invalid
57
+ def document!(name, preambles: [], attributes: {})
58
+ Graphics::Document.define(name, preambles:, attributes:, overwrite: true)
59
+ end
60
+
61
+ # Defines a paper profile unless the same profile already exists.
62
+ # @param width [Numeric] paper width
63
+ # @param height [Numeric] paper height
64
+ # @param name [Symbol, String] profile name
65
+ # @param unit [Symbol, String] SVG unit
66
+ # @return [Symbol, String] original profile name
67
+ # @raise [Sevgi::ArgumentError] when the profile is invalid or an existing profile has different dimensions
24
68
  def paper(width, height, name = :custom, unit: "mm")
25
- name.tap { Graphics::Paper.define(name, width:, height:, unit:) unless Graphics::Paper.exist?(name) }
69
+ profile = Graphics::Paper[width, height, unit, name]
70
+
71
+ if Graphics::Paper.exist?(name)
72
+ ArgumentError.("Paper already defined differently: #{name}") unless Graphics::Paper.public_send(name) == profile
73
+ else
74
+ Graphics::Paper.define(name, width:, height:, unit:)
75
+ end
76
+
77
+ name
26
78
  end
27
79
 
80
+ # Defines or replaces a paper profile.
81
+ # @param width [Numeric] paper width
82
+ # @param height [Numeric] paper height
83
+ # @param name [Symbol, String] profile name
84
+ # @param unit [Symbol, String] SVG unit
85
+ # @return [Symbol, String] original profile name
86
+ # @raise [Sevgi::ArgumentError] when the profile is invalid or the profile name is reserved
28
87
  def paper!(width, height, name = :custom, unit: "mm")
29
88
  name.tap { Graphics::Paper.define(name, width:, height:, unit:) }
30
89
  end
31
90
 
91
+ # Builds an SVG root document.
92
+ # @param document [Symbol, String, Class] document profile name or document class
93
+ # @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
94
+ # @param block [Proc, nil] DSL block evaluated in the root element
95
+ # @return [Sevgi::Graphics::Document::Proto] SVG root element
96
+ # @raise [Sevgi::ArgumentError] when the document or canvas profile is unknown
32
97
  def SVG(document = :default, canvas = Undefined, **, &block)
33
98
  Graphics::Document.(document, canvas, **, &block)
34
99
  end
@@ -36,5 +101,6 @@ module Sevgi
36
101
  extend self
37
102
  end
38
103
 
104
+ # Top-level alias for the graphics DSL namespace.
39
105
  SVG = Graphics
40
106
  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.73.2
4
+ version: 0.94.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,27 +15,30 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.73.2
18
+ version: 0.94.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.73.2
25
+ version: 0.94.0
26
26
  description: Provides an extensible DSL for creating SVG content.
27
27
  email: roktas@gmail.com
28
28
  executables: []
29
29
  extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
+ - CHANGELOG.md
33
+ - LICENSE
34
+ - README.md
32
35
  - lib/sevgi/graphics.rb
33
36
  - lib/sevgi/graphics/attribute.rb
34
- - lib/sevgi/graphics/auxilary.rb
35
- - lib/sevgi/graphics/auxilary/canvas.rb
36
- - lib/sevgi/graphics/auxilary/content.rb
37
- - lib/sevgi/graphics/auxilary/margin.rb
38
- - lib/sevgi/graphics/auxilary/paper.rb
37
+ - lib/sevgi/graphics/auxiliary.rb
38
+ - lib/sevgi/graphics/auxiliary/canvas.rb
39
+ - lib/sevgi/graphics/auxiliary/content.rb
40
+ - lib/sevgi/graphics/auxiliary/margin.rb
41
+ - lib/sevgi/graphics/auxiliary/paper.rb
39
42
  - lib/sevgi/graphics/document.rb
40
43
  - lib/sevgi/graphics/document/base.rb
41
44
  - lib/sevgi/graphics/document/default.rb
@@ -79,7 +82,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
82
  requirements:
80
83
  - - ">="
81
84
  - !ruby/object:Gem::Version
82
- version: 3.4.0.pre.preview1
85
+ version: 3.4.0
83
86
  required_rubygems_version: !ruby/object:Gem::Requirement
84
87
  requirements:
85
88
  - - ">="
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "forwardable"
4
-
5
- module Sevgi
6
- module Graphics
7
- class Canvas
8
- def self.call(arg = Undefined, **kwargs)
9
- case arg
10
- when Undefined
11
- new(**kwargs)
12
- when ::Symbol, ::String
13
- new(**Paper.public_send(arg).to_h, **kwargs)
14
- else
15
- ArgumentError.("Argument must be a Paper symbol: #{arg}")
16
- end
17
- end
18
-
19
- extend Forwardable
20
-
21
- def_delegators :@margin, *Margin.members
22
- def_delegators :@size, *Paper.members
23
-
24
- attr_reader :size, :margin, :inner
25
-
26
- def initialize(width:, height:, unit: "mm", name: :custom, margins: [])
27
- @size = Paper[width, height, unit, name]
28
- @margin = Margin[*margins]
29
-
30
- compute
31
- freeze
32
- end
33
-
34
- def attributes(...) = {**viewport, viewBox: viewbox(...)}
35
-
36
- def conforming(...) = self.class.conforming(self, ...)
37
-
38
- def viewport = {width: "#{width}#{unit}", height: "#{height}#{unit}"}
39
-
40
- def viewbox(origin = Undefined) = prettify(*originate(origin), width, height).join(" ")
41
-
42
- def with(**kwargs) = self.class.new(**size.to_h, margins: kwargs.fetch(:margins, margin.to_a))
43
-
44
- private
45
-
46
- def compute
47
- @inner = size.with(width: width - margin.left - margin.right, height: height - margin.top - margin.bottom)
48
- end
49
-
50
- def originate(origin)
51
- case origin
52
- when Undefined
53
- [-margin.left, -margin.top]
54
- when ::Numeric, ::NilClass
55
- [origin.to_f, origin.to_f]
56
- when ::Array
57
- pair(origin)
58
- else
59
- ArgumentError.("Canvas origin must be an Array")
60
- end
61
- end
62
-
63
- def prettify(*floats)
64
- floats.map { (it % 1).zero? ? it.to_i : it }
65
- end
66
-
67
- def pair(array)
68
- array.size == 2 ? array.map(&:to_f) : ArgumentError.("Argument must be an Array of size 2")
69
- end
70
- end
71
- end
72
- end