sevgi-graphics 0.94.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +275 -2
- data/LICENSE +672 -3
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +271 -54
- data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
- data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
- data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
- data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +3 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +370 -124
- data/lib/sevgi/graphics/element.rb +143 -33
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +143 -33
- data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
- data/lib/sevgi/graphics/mixtures/export.rb +58 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +40 -5
- data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
- data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
- data/lib/sevgi/graphics/mixtures/render.rb +88 -19
- data/lib/sevgi/graphics/mixtures/save.rb +79 -26
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
- data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
- data/lib/sevgi/graphics/mixtures.rb +19 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +75 -28
- metadata +10 -6
|
@@ -6,27 +6,42 @@ 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
|
-
# @
|
|
10
|
-
#
|
|
11
|
-
# @param
|
|
12
|
-
# @param
|
|
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
|
|
|
27
40
|
# Builds a titled SVG symbol.
|
|
28
41
|
# @param name [String] human-readable symbol name
|
|
29
42
|
# @param kwargs [Hash] symbol attributes
|
|
43
|
+
# @yield evaluates the drawing DSL in the symbol element
|
|
44
|
+
# @yieldreturn [Object] ignored block result
|
|
30
45
|
# @return [Sevgi::Graphics::Element] symbol element
|
|
31
46
|
def Symbol(name, **kwargs, &block)
|
|
32
47
|
id = (words = name.split).map(&:downcase).join("-")
|
|
@@ -39,23 +54,36 @@ module Sevgi
|
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
# 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
|
|
57
|
+
# @param y2 [Numeric] finite ending y coordinate
|
|
58
|
+
# @param x1 [Numeric] finite starting x coordinate
|
|
59
|
+
# @param y1 [Numeric] finite starting y coordinate
|
|
45
60
|
# @return [Sevgi::Graphics::Element] path element
|
|
61
|
+
# @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
|
|
46
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
|
+
|
|
47
67
|
path(d: "M #{x1} #{y1} V #{y2}", **)
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
# 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
|
|
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
|
|
55
75
|
# @return [Sevgi::Graphics::Element] path element
|
|
76
|
+
# @raise [Sevgi::ArgumentError] when an operand is not a finite real number
|
|
56
77
|
def LineBy(angle:, length:, x: 0, y: 0, **)
|
|
57
|
-
|
|
58
|
-
dy =
|
|
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
|
+
|
|
59
87
|
path(d: "M #{x} #{y} l #{dx} #{dy}", **)
|
|
60
88
|
end
|
|
61
89
|
|
|
@@ -63,7 +91,8 @@ module Sevgi
|
|
|
63
91
|
# @param hash [Hash, nil] CSS rules
|
|
64
92
|
# @param attributes [Hash] style attributes or CSS rules when hash is nil
|
|
65
93
|
# @return [Sevgi::Graphics::Element] style element
|
|
66
|
-
# @raise [Sevgi::ArgumentError] when CSS
|
|
94
|
+
# @raise [Sevgi::ArgumentError] when CSS rules are malformed, cyclic, cannot be stringified, or contain invalid
|
|
95
|
+
# encoding or illegal XML 1.0 characters
|
|
67
96
|
def css(hash = nil, **attributes)
|
|
68
97
|
hash, attributes = attributes, {} unless hash
|
|
69
98
|
|
|
@@ -71,27 +100,40 @@ module Sevgi
|
|
|
71
100
|
end
|
|
72
101
|
|
|
73
102
|
# 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
|
|
103
|
+
# @param length [Numeric] finite line length
|
|
104
|
+
# @param x [Numeric] finite starting x coordinate
|
|
105
|
+
# @param y [Numeric] finite starting y coordinate
|
|
77
106
|
# @return [Sevgi::Graphics::Element] path element
|
|
107
|
+
# @raise [Sevgi::ArgumentError] when an operand is not a finite real number
|
|
78
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
|
+
|
|
79
113
|
path(d: "M #{x} #{y} h #{length}", **)
|
|
80
114
|
end
|
|
81
115
|
|
|
82
116
|
# Builds a square rect.
|
|
83
|
-
# @param length [Numeric] side length
|
|
117
|
+
# @param length [Numeric] finite side length
|
|
84
118
|
# @return [Sevgi::Graphics::Element] rect element
|
|
119
|
+
# @raise [Sevgi::ArgumentError] when length is not a finite real number
|
|
85
120
|
def square(length:, **)
|
|
121
|
+
length = Scalar.number(length, context: "square", field: :length)
|
|
122
|
+
|
|
86
123
|
rect(width: length, height: length, **)
|
|
87
124
|
end
|
|
88
125
|
|
|
89
126
|
# 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
|
|
127
|
+
# @param length [Numeric] finite line length
|
|
128
|
+
# @param x [Numeric] finite starting x coordinate
|
|
129
|
+
# @param y [Numeric] finite starting y coordinate
|
|
93
130
|
# @return [Sevgi::Graphics::Element] path element
|
|
131
|
+
# @raise [Sevgi::ArgumentError] when an operand is not a finite real number
|
|
94
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
|
+
|
|
95
137
|
path(d: "M #{x} #{y} v #{length}", **)
|
|
96
138
|
end
|
|
97
139
|
end
|
|
@@ -8,37 +8,43 @@ module Sevgi
|
|
|
8
8
|
# Includes a named mixture and optional anonymous extension into a document class.
|
|
9
9
|
# @param mod [Symbol, String] mixture constant name
|
|
10
10
|
# @param document [Class] document class receiving the mixture
|
|
11
|
+
# @yield defines methods in the anonymous mixture
|
|
12
|
+
# @yieldreturn [Object] ignored module-definition result
|
|
11
13
|
# @return [Module, nil] optional anonymous mixture when a block is given
|
|
12
14
|
# @raise [NameError] when the mixture does not exist
|
|
13
15
|
# @overload mixin(document = Graphics::Document::Base, &block)
|
|
14
16
|
# Includes only an anonymous extension into a document class.
|
|
15
17
|
# @param document [Class] document class receiving the mixture
|
|
18
|
+
# @yield defines methods in the anonymous mixture
|
|
19
|
+
# @yieldreturn [Object] ignored module-definition result
|
|
16
20
|
# @return [Module] anonymous mixture
|
|
17
21
|
# @raise [Sevgi::ArgumentError] when no named mixture or block is given
|
|
18
22
|
def self.mixin(mod = Undefined, document = Graphics::Document::Base, &block)
|
|
19
23
|
mod, document = normalize(mod, document, block)
|
|
20
24
|
ArgumentError.("Mixture name or block required") if mod == Undefined && !block
|
|
21
25
|
|
|
22
|
-
document.mixture
|
|
26
|
+
document.send(:mixture, mod) unless mod == Undefined
|
|
23
27
|
include_anonymous(document, &block) if block
|
|
24
28
|
end
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
30
|
+
class << self
|
|
31
|
+
private
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
document.send(:include, anonymous)
|
|
33
|
-
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
|
|
34
35
|
end
|
|
35
|
-
end
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
def normalize(mod, document, block)
|
|
45
|
+
anonymous_document?(mod, block) ? [Undefined, mod] : [mod, document]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# XML 1.0 text and qualified-name validation shared by renderer boundaries.
|
|
6
|
+
# @api private
|
|
7
|
+
module XML
|
|
8
|
+
NCNAME_START = "A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}" \
|
|
9
|
+
"\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}" \
|
|
10
|
+
"\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}"
|
|
11
|
+
NCNAME_CHAR = "#{NCNAME_START}\\-.0-9\u{B7}\u{300}-\u{36F}\u{203F}-\u{2040}".freeze
|
|
12
|
+
NCNAME = "[#{NCNAME_START}][#{NCNAME_CHAR}]*".freeze
|
|
13
|
+
QNAME = /\A#{NCNAME}(?::#{NCNAME})?\z/u
|
|
14
|
+
|
|
15
|
+
private_constant :NCNAME, :NCNAME_CHAR, :NCNAME_START, :QNAME
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Validates an XML 1.0 string representation.
|
|
19
|
+
# @param value [Object] value to stringify and validate
|
|
20
|
+
# @param context [String] error-message subject
|
|
21
|
+
# @return [String] UTF-8 text
|
|
22
|
+
# @raise [Sevgi::ArgumentError] when stringification, encoding, or XML character validation fails
|
|
23
|
+
def text(value, context: "XML content")
|
|
24
|
+
value = stringify(value, context:) unless value.is_a?(::String)
|
|
25
|
+
validate_string(value, context:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validates and snapshots a nested XML-bound value.
|
|
29
|
+
# @param value [Object] scalar or nested container
|
|
30
|
+
# @param context [String] error-message subject
|
|
31
|
+
# @param seen [Hash] container identities on the current traversal path
|
|
32
|
+
# @return [Object] validated caller-owned snapshot
|
|
33
|
+
# @raise [Sevgi::ArgumentError] when the value is cyclic, collides after stringification, or has invalid XML text
|
|
34
|
+
def snapshot(value, context:, seen: {}.compare_by_identity)
|
|
35
|
+
case value
|
|
36
|
+
when ::Hash
|
|
37
|
+
nested(value, context:, seen:) { snapshot_hash(value, context:, seen:) }
|
|
38
|
+
when ::Array
|
|
39
|
+
nested(value, context:, seen:) { value.map { snapshot(it, context:, seen:) } }
|
|
40
|
+
else
|
|
41
|
+
text(value, context:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Validates an XML qualified name.
|
|
46
|
+
# @param value [Object] candidate name
|
|
47
|
+
# @param context [String] error-message subject
|
|
48
|
+
# @return [String] validated qualified name
|
|
49
|
+
# @raise [Sevgi::ArgumentError] when text conversion or QName validation fails
|
|
50
|
+
def name(value, context: "XML name")
|
|
51
|
+
text(value, context:).tap do |candidate|
|
|
52
|
+
ArgumentError.("#{context} is invalid: #{candidate.inspect}") unless QNAME.match?(candidate)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validates a nested XML value without retaining its snapshot.
|
|
57
|
+
# @param value [Object] scalar or nested container
|
|
58
|
+
# @param context [String] error-message subject
|
|
59
|
+
# @return [Object] original value
|
|
60
|
+
# @raise [Sevgi::ArgumentError] when nested content is cyclic or contains invalid XML text
|
|
61
|
+
def validate(value, context: "XML content")
|
|
62
|
+
snapshot(value, context:)
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Escapes embedded CDATA terminators after validating text.
|
|
67
|
+
# @param value [Object] CDATA value
|
|
68
|
+
# @return [String] safe CDATA body
|
|
69
|
+
# @raise [Sevgi::ArgumentError] when value is not valid XML text
|
|
70
|
+
def cdata(value) = text(value).gsub("]]>", "]]]]><![CDATA[>")
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def nested(value, context:, seen:)
|
|
75
|
+
ArgumentError.("Cyclic #{context} is not supported") if seen.key?(value)
|
|
76
|
+
|
|
77
|
+
seen[value] = true
|
|
78
|
+
yield
|
|
79
|
+
ensure
|
|
80
|
+
seen.delete(value)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def snapshot_hash(value, context:, seen:)
|
|
84
|
+
value.each_with_object({}) do |(key, item), captured|
|
|
85
|
+
key = snapshot(key, context:, seen:)
|
|
86
|
+
ArgumentError.("#{context} keys collide after stringification") if captured.key?(key)
|
|
87
|
+
|
|
88
|
+
captured[key] = snapshot(item, context:, seen:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def stringify(value, context:)
|
|
93
|
+
text = value.to_s
|
|
94
|
+
ArgumentError.("#{context} stringification must return a String") unless text.is_a?(::String)
|
|
95
|
+
|
|
96
|
+
text
|
|
97
|
+
rescue Sevgi::ArgumentError
|
|
98
|
+
raise
|
|
99
|
+
rescue ::StandardError => e
|
|
100
|
+
ArgumentError.("#{context} cannot be stringified: #{e.class}: #{e.message}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_string(value, context:)
|
|
104
|
+
ArgumentError.("#{context} must be valid UTF-8") unless value.valid_encoding?
|
|
105
|
+
|
|
106
|
+
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)}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
text
|
|
112
|
+
rescue ::EncodingError => e
|
|
113
|
+
ArgumentError.("#{context} must be valid UTF-8: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private_constant :XML
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/sevgi/graphics.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "sevgi/function"
|
|
4
4
|
|
|
5
|
+
require_relative "graphics/xml"
|
|
5
6
|
require_relative "graphics/attribute"
|
|
6
7
|
require_relative "graphics/auxiliary"
|
|
7
8
|
require_relative "graphics/element"
|
|
@@ -13,15 +14,44 @@ require_relative "graphics/version"
|
|
|
13
14
|
|
|
14
15
|
module Sevgi
|
|
15
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
|
|
16
33
|
module Graphics
|
|
17
|
-
# @overload canvas(
|
|
18
|
-
# Builds a canvas from a paper profile
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
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
|
|
21
38
|
# @return [Sevgi::Graphics::Canvas]
|
|
22
|
-
# @raise [Sevgi::ArgumentError] when the paper
|
|
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"}]
|
|
23
53
|
def canvas(...)
|
|
24
|
-
Graphics::Canvas.
|
|
54
|
+
Graphics::Canvas.call(...)
|
|
25
55
|
end
|
|
26
56
|
|
|
27
57
|
# @overload document(name)
|
|
@@ -30,35 +60,55 @@ module Sevgi
|
|
|
30
60
|
# @return [Class] document class
|
|
31
61
|
# @raise [Sevgi::ArgumentError] when the profile is unknown
|
|
32
62
|
# @overload document(name, preambles: Undefined, attributes: Undefined)
|
|
33
|
-
#
|
|
34
|
-
#
|
|
63
|
+
# Looks up a named profile when both definition keywords are omitted. Supplying `preambles:` or `attributes:`
|
|
64
|
+
# defines a named profile, or returns an existing profile when every explicitly supplied field matches. Omitted
|
|
65
|
+
# fields are ignored during existing-profile comparison. Profile containers and strings are copied into the
|
|
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.
|
|
35
70
|
# @param name [Symbol, String] profile name
|
|
36
71
|
# @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
|
|
37
|
-
# @param attributes [Hash, Sevgi::Undefined] default root attributes
|
|
72
|
+
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes; nil means an empty Hash
|
|
38
73
|
# @return [Class] document class
|
|
39
|
-
# @raise [Sevgi::ArgumentError] when a
|
|
74
|
+
# @raise [Sevgi::ArgumentError] when a profile conflicts or metadata is invalid XML, cyclic, or cannot be stringified
|
|
40
75
|
# @overload document(preambles: Undefined, attributes: Undefined)
|
|
41
76
|
# Defines an anonymous document profile without registering it globally.
|
|
42
77
|
# @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
|
|
43
|
-
# @param attributes [Hash, Sevgi::Undefined] default root attributes
|
|
78
|
+
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes; nil means an empty Hash
|
|
44
79
|
# @return [Class] anonymous document class
|
|
45
|
-
# @raise [Sevgi::ArgumentError] when
|
|
80
|
+
# @raise [Sevgi::ArgumentError] when metadata is invalid XML, cyclic, or cannot be stringified
|
|
46
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"
|
|
47
93
|
def document(name = Undefined, preambles: Undefined, attributes: Undefined)
|
|
48
94
|
Graphics::Document.define(name, preambles:, attributes:)
|
|
49
95
|
end
|
|
50
96
|
|
|
51
97
|
# Defines or replaces a document profile class.
|
|
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.
|
|
52
101
|
# @param name [Symbol, String] profile name
|
|
53
102
|
# @param preambles [Array<String>, nil] document preamble lines
|
|
54
|
-
# @param attributes [Hash] default root attributes
|
|
103
|
+
# @param attributes [Hash, nil] default root attributes; nil means an empty Hash
|
|
55
104
|
# @return [Class] document class
|
|
56
|
-
# @raise [Sevgi::ArgumentError] when the
|
|
105
|
+
# @raise [Sevgi::ArgumentError] when the name or metadata is invalid XML, cyclic, or cannot be stringified
|
|
57
106
|
def document!(name, preambles: [], attributes: {})
|
|
58
107
|
Graphics::Document.define(name, preambles:, attributes:, overwrite: true)
|
|
59
108
|
end
|
|
60
109
|
|
|
61
|
-
# Defines a paper profile unless the same profile already exists.
|
|
110
|
+
# Defines a paper profile unless the same profile already exists. Registration is process-global and thread-atomic;
|
|
111
|
+
# an identical concurrent definition is idempotent and a conflicting definition is rejected.
|
|
62
112
|
# @param width [Numeric] paper width
|
|
63
113
|
# @param height [Numeric] paper height
|
|
64
114
|
# @param name [Symbol, String] profile name
|
|
@@ -66,18 +116,13 @@ module Sevgi
|
|
|
66
116
|
# @return [Symbol, String] original profile name
|
|
67
117
|
# @raise [Sevgi::ArgumentError] when the profile is invalid or an existing profile has different dimensions
|
|
68
118
|
def paper(width, height, name = :custom, unit: "mm")
|
|
69
|
-
|
|
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
|
|
119
|
+
Graphics::Paper.define(name, width:, height:, unit:, overwrite: false)
|
|
76
120
|
|
|
77
121
|
name
|
|
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,15 +130,19 @@ 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
|
|
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
|
|
143
|
+
# @yieldreturn [Object] ignored block result
|
|
95
144
|
# @return [Sevgi::Graphics::Document::Proto] SVG root element
|
|
96
|
-
# @raise [Sevgi::ArgumentError] when the document or
|
|
145
|
+
# @raise [Sevgi::ArgumentError] when the document/canvas profile or root XML attributes are invalid
|
|
97
146
|
def SVG(document = :default, canvas = Undefined, **, &block)
|
|
98
147
|
Graphics::Document.(document, canvas, **, &block)
|
|
99
148
|
end
|
|
@@ -101,6 +150,4 @@ module Sevgi
|
|
|
101
150
|
extend self
|
|
102
151
|
end
|
|
103
152
|
|
|
104
|
-
# Top-level alias for the graphics DSL namespace.
|
|
105
|
-
SVG = Graphics
|
|
106
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.
|
|
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.
|
|
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.
|
|
26
|
-
description:
|
|
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,9 @@ 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
|
|
43
|
+
- lib/sevgi/graphics/auxiliary/scalar.rb
|
|
44
|
+
- lib/sevgi/graphics/auxiliary/sizes.rb
|
|
42
45
|
- lib/sevgi/graphics/document.rb
|
|
43
46
|
- lib/sevgi/graphics/document/base.rb
|
|
44
47
|
- lib/sevgi/graphics/document/default.rb
|
|
@@ -67,6 +70,7 @@ files:
|
|
|
67
70
|
- lib/sevgi/graphics/mixtures/validate.rb
|
|
68
71
|
- lib/sevgi/graphics/mixtures/wrappers.rb
|
|
69
72
|
- lib/sevgi/graphics/version.rb
|
|
73
|
+
- lib/sevgi/graphics/xml.rb
|
|
70
74
|
homepage: https://sevgi.roktas.dev
|
|
71
75
|
licenses:
|
|
72
76
|
- GPL-3.0-or-later
|
|
@@ -89,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
89
93
|
- !ruby/object:Gem::Version
|
|
90
94
|
version: '0'
|
|
91
95
|
requirements: []
|
|
92
|
-
rubygems_version: 4.0.
|
|
96
|
+
rubygems_version: 4.0.16
|
|
93
97
|
specification_version: 4
|
|
94
|
-
summary: Core DSL for
|
|
98
|
+
summary: Core SVG DSL for Sevgi.
|
|
95
99
|
test_files: []
|