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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +160 -37
- data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
- data/lib/sevgi/graphics/auxiliary/content.rb +54 -34
- data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
- data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
- data/lib/sevgi/graphics/auxiliary.rb +1 -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 +236 -102
- data/lib/sevgi/graphics/element.rb +114 -31
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +59 -20
- data/lib/sevgi/graphics/mixtures/duplicate.rb +49 -15
- data/lib/sevgi/graphics/mixtures/export.rb +52 -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 +26 -8
- data/lib/sevgi/graphics/mixtures/inkscape.rb +201 -47
- data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
- data/lib/sevgi/graphics/mixtures/render.rb +52 -28
- data/lib/sevgi/graphics/mixtures/save.rb +79 -35
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
- data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +14 -5
- data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +62 -23
- data/lib/sevgi/graphics/mixtures.rb +15 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +58 -12
- 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
|
-
|
|
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#{
|
|
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#{
|
|
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#{
|
|
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
|
|
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#{
|
|
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
|
-
|
|
82
|
+
angle = Scalar.number(a, context: "rotation", field: :angle)
|
|
83
|
+
origin = Scalar.numbers(origin, context: "rotation")
|
|
73
84
|
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
angle = Scalar.number(a, context: "skew", field: :x)
|
|
134
|
+
next if angle.zero?
|
|
114
135
|
|
|
115
|
-
attributes[:"transform#{
|
|
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
|
-
|
|
146
|
+
angle = Scalar.number(a, context: "skew", field: :y)
|
|
147
|
+
next if angle.zero?
|
|
125
148
|
|
|
126
|
-
attributes[:"transform#{
|
|
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
|
-
|
|
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#{
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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[
|
|
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
|
-
|
|
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.
|
|
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
|
-
# @
|
|
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
|
|
|
@@ -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
|
-
|
|
60
|
-
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
|
+
|
|
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
|
|
26
|
+
document.send(:mixture, mod) unless mod == Undefined
|
|
27
27
|
include_anonymous(document, &block) if block
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
30
|
+
class << self
|
|
31
|
+
private
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
data/lib/sevgi/graphics.rb
CHANGED
|
@@ -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(
|
|
19
|
-
# Builds a canvas from a paper profile
|
|
20
|
-
# @param
|
|
21
|
-
# @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
|
|
22
38
|
# @return [Sevgi::Graphics::Canvas]
|
|
23
|
-
# @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"}]
|
|
24
53
|
def canvas(...)
|
|
25
|
-
Graphics::Canvas.
|
|
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;
|
|
38
|
-
#
|
|
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
|
|
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.
|
|
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,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.
|
|
96
|
+
rubygems_version: 4.0.16
|
|
96
97
|
specification_version: 4
|
|
97
|
-
summary: Core DSL for
|
|
98
|
+
summary: Core SVG DSL for Sevgi.
|
|
98
99
|
test_files: []
|