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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +221 -2
- data/README.md +12 -9
- data/lib/sevgi/graphics/attribute.rb +166 -45
- data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
- data/lib/sevgi/graphics/auxiliary/content.rb +56 -47
- 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 +239 -117
- data/lib/sevgi/graphics/element.rb +132 -34
- data/lib/sevgi/graphics/mixtures/call.rb +234 -88
- data/lib/sevgi/graphics/mixtures/core.rb +67 -27
- data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -25
- data/lib/sevgi/graphics/mixtures/export.rb +54 -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 +214 -47
- data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
- data/lib/sevgi/graphics/mixtures/render.rb +60 -120
- 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 +89 -30
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +111 -23
- data/lib/sevgi/graphics/mixtures.rb +15 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +4 -9
- data/lib/sevgi/graphics.rb +69 -18
- metadata +7 -6
|
@@ -3,22 +3,35 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# DSL helpers for defining one SVG template and repeating it through `use` elements.
|
|
7
|
+
#
|
|
8
|
+
# Use {Sevgi::Sundries::Tile} instead when Ruby code needs inspectable repeated geometry or row/column bounds
|
|
9
|
+
# rather than SVG references.
|
|
10
|
+
# @see Sevgi::Sundries::Tile
|
|
11
|
+
# @see https://sevgi.roktas.dev/layout/#choose-a-layout-model Choosing a layout model
|
|
8
12
|
module Tile
|
|
9
|
-
#
|
|
13
|
+
# Stable prefix used for generated tile CSS classes.
|
|
10
14
|
PREFIX = "tile"
|
|
11
15
|
|
|
12
16
|
# Builds a two-dimensional tile grid.
|
|
17
|
+
# Each use id has the form `id-row-column`, with one-based row and column numbers. Generated classes identify
|
|
18
|
+
# the one-based row and column and mark their first and last positions. A block defines the referenced template
|
|
19
|
+
# as a group under `defs` before the uses are added.
|
|
20
|
+
# @example Define and customize a tile grid
|
|
21
|
+
# customize = proc { |use, x:, y:, nx:, ny:| use[:opacity] = (x + y + 1).fdiv(nx + ny) }
|
|
22
|
+
# Sevgi::Graphics.SVG(:minimal) do
|
|
23
|
+
# Tile("dot", nx: 2, dx: 10, ny: 2, dy: 10, proc: customize) { circle r: 2 }
|
|
24
|
+
# end
|
|
13
25
|
# @param id [String] referenced template id
|
|
14
26
|
# @param nx [Integer] number of columns
|
|
15
|
-
# @param dx [Numeric] horizontal spacing
|
|
16
|
-
# @param ox [Numeric] horizontal offset
|
|
27
|
+
# @param dx [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
28
|
+
# @param ox [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
17
29
|
# @param ny [Integer] number of rows
|
|
18
|
-
# @param dy [Numeric] vertical spacing
|
|
19
|
-
# @param oy [Numeric] vertical offset
|
|
20
|
-
# @param proc [Proc, nil] optional
|
|
21
|
-
#
|
|
30
|
+
# @param dy [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
31
|
+
# @param oy [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
32
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, y:, nx:, ny:)`, with
|
|
33
|
+
# zero-based coordinates and total counts. The callback can mutate the element. Its return value is ignored
|
|
34
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
22
35
|
# @yieldreturn [Object] ignored block result
|
|
23
36
|
# @return [Sevgi::Graphics::Element] self
|
|
24
37
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
@@ -33,14 +46,9 @@ module Sevgi
|
|
|
33
46
|
proc: nil,
|
|
34
47
|
&block
|
|
35
48
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# rubocop:disable Style/NestedTernaryOperator
|
|
40
|
-
# for pretty kwargs handling
|
|
41
|
-
x.zero? ? (y.zero? ? {} : {y:}) : (y.zero? ? {x:} : {x:, y:})
|
|
42
|
-
# rubocop:enable Style/NestedTernaryOperator
|
|
43
|
-
end
|
|
49
|
+
id, nx, dx, ox, ny, dy, oy, callback = Helper
|
|
50
|
+
.normalize(id:, nx:, dx:, ox:, ny:, dy:, oy:, proc:)
|
|
51
|
+
.values_at(:id, :nx, :dx, :ox, :ny, :dy, :oy, :proc)
|
|
44
52
|
|
|
45
53
|
defs { g(id:, &block) } if block
|
|
46
54
|
|
|
@@ -52,34 +60,36 @@ module Sevgi
|
|
|
52
60
|
cs = Helper.classify(as: "col", index: x, upper: nx)
|
|
53
61
|
|
|
54
62
|
element = use(
|
|
55
|
-
id: [
|
|
56
|
-
href: "##{
|
|
63
|
+
id: [id, y + 1, x + 1].join("-"),
|
|
64
|
+
href: "##{id}",
|
|
57
65
|
class: [*rs, *cs].join(" "),
|
|
58
|
-
**
|
|
66
|
+
**Helper.coordinates(
|
|
67
|
+
x: Scalar.number((x * dx) + ox, context: "tile", field: :x),
|
|
68
|
+
y: Scalar.number((y * dy) + oy, context: "tile", field: :y)
|
|
69
|
+
)
|
|
59
70
|
)
|
|
60
|
-
|
|
71
|
+
callback&.call(element, x:, y:, nx:, ny:)
|
|
61
72
|
end
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
# Builds a one-dimensional horizontal tile row.
|
|
78
|
+
# Each use id has the form `id-column`, with a one-based column number. Generated classes identify the column and
|
|
79
|
+
# mark its first and last positions. A block defines the referenced template as a group under `defs` before the
|
|
80
|
+
# uses are added.
|
|
67
81
|
# @param id [String] referenced template id
|
|
68
82
|
# @param n [Integer] number of instances
|
|
69
|
-
# @param d [Numeric] horizontal spacing
|
|
70
|
-
# @param o [Numeric] horizontal offset
|
|
71
|
-
# @param proc [Proc, nil] optional
|
|
72
|
-
#
|
|
83
|
+
# @param d [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
84
|
+
# @param o [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
85
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, n:)`, with a zero-based column
|
|
86
|
+
# and total count. The callback can mutate the element. Its return value is ignored
|
|
87
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
73
88
|
# @yieldreturn [Object] ignored block result
|
|
74
89
|
# @return [Sevgi::Graphics::Element] self
|
|
75
90
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
76
91
|
def TileX(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
77
|
-
Helper.
|
|
78
|
-
|
|
79
|
-
href, coords = id, proc do |x|
|
|
80
|
-
# for pretty kwargs handling
|
|
81
|
-
x.zero? ? {} : {x:}
|
|
82
|
-
end
|
|
92
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
83
93
|
|
|
84
94
|
defs { g(id:, &block) } if block
|
|
85
95
|
|
|
@@ -88,33 +98,32 @@ module Sevgi
|
|
|
88
98
|
cs = Helper.classify(as: "col", index: x, upper: n)
|
|
89
99
|
|
|
90
100
|
element = use(
|
|
91
|
-
id: [
|
|
92
|
-
href: "##{
|
|
101
|
+
id: [id, x + 1].join("-"),
|
|
102
|
+
href: "##{id}",
|
|
93
103
|
class: cs.join(" "),
|
|
94
|
-
**
|
|
104
|
+
**Helper.coordinates(x: Scalar.number((x * d) + o, context: "tile", field: :x))
|
|
95
105
|
)
|
|
96
|
-
|
|
106
|
+
callback&.call(element, x:, n:)
|
|
97
107
|
end
|
|
98
108
|
end
|
|
99
109
|
end
|
|
100
110
|
|
|
101
111
|
# Builds a one-dimensional vertical tile column.
|
|
112
|
+
# Each use id has the form `id-row`, with a one-based row number. Generated classes identify the row and mark its
|
|
113
|
+
# first and last positions. A block defines the referenced template as a group under `defs` before the uses are
|
|
114
|
+
# added.
|
|
102
115
|
# @param id [String] referenced template id
|
|
103
116
|
# @param n [Integer] number of instances
|
|
104
|
-
# @param d [Numeric] vertical spacing
|
|
105
|
-
# @param o [Numeric] vertical offset
|
|
106
|
-
# @param proc [Proc, nil] optional
|
|
107
|
-
#
|
|
117
|
+
# @param d [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
118
|
+
# @param o [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
119
|
+
# @param proc [Proc, nil] optional callback invoked for each use as `(element, y:, n:)`, with a zero-based row and
|
|
120
|
+
# total count. The callback can mutate the element. Its return value is ignored
|
|
121
|
+
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
108
122
|
# @yieldreturn [Object] ignored block result
|
|
109
123
|
# @return [Sevgi::Graphics::Element] self
|
|
110
124
|
# @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
|
|
111
125
|
def TileY(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
|
|
112
|
-
Helper.
|
|
113
|
-
|
|
114
|
-
href, coords = id, proc do |y|
|
|
115
|
-
# for pretty kwargs handling
|
|
116
|
-
y.zero? ? {} : {y:}
|
|
117
|
-
end
|
|
126
|
+
id, n, d, o, callback = Helper.normalize(id:, n:, d:, o:, proc:).values_at(:id, :n, :d, :o, :proc)
|
|
118
127
|
|
|
119
128
|
defs { g(id:, &block) } if block
|
|
120
129
|
|
|
@@ -123,12 +132,12 @@ module Sevgi
|
|
|
123
132
|
rs = Helper.classify(as: "row", index: y, upper: n)
|
|
124
133
|
|
|
125
134
|
element = use(
|
|
126
|
-
id: [
|
|
127
|
-
href: "##{
|
|
135
|
+
id: [id, y + 1].join("-"),
|
|
136
|
+
href: "##{id}",
|
|
128
137
|
class: rs.join(" "),
|
|
129
|
-
**
|
|
138
|
+
**Helper.coordinates(y: Scalar.number((y * d) + o, context: "tile", field: :y))
|
|
130
139
|
)
|
|
131
|
-
|
|
140
|
+
callback&.call(element, y:, n:)
|
|
132
141
|
end
|
|
133
142
|
end
|
|
134
143
|
end
|
|
@@ -138,18 +147,14 @@ module Sevgi
|
|
|
138
147
|
module Helper
|
|
139
148
|
extend self
|
|
140
149
|
|
|
150
|
+
FINITE = %i[d dx dy o ox oy].freeze
|
|
151
|
+
|
|
141
152
|
# Argument validators for tile helpers.
|
|
142
153
|
ASSERTION = {
|
|
143
154
|
id: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
|
|
144
155
|
n: proc { |name, value| positive_integer_issue(name, value) },
|
|
145
156
|
nx: proc { |name, value| positive_integer_issue(name, value) },
|
|
146
157
|
ny: proc { |name, value| positive_integer_issue(name, value) },
|
|
147
|
-
d: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
148
|
-
dx: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
149
|
-
dy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
150
|
-
o: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
151
|
-
ox: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
152
|
-
oy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
|
153
158
|
proc: proc { |name, value| "Argument '#{name}' must be a proc" unless value.nil? || value.is_a?(::Proc) }
|
|
154
159
|
}.freeze
|
|
155
160
|
|
|
@@ -161,22 +166,33 @@ module Sevgi
|
|
|
161
166
|
"Argument '#{name}' must be a positive integer" unless value.is_a?(::Integer) && value.positive?
|
|
162
167
|
end
|
|
163
168
|
|
|
164
|
-
# Validates tile arguments.
|
|
169
|
+
# Validates and normalizes tile arguments.
|
|
165
170
|
# @param kwargs [Hash] tile arguments
|
|
166
|
-
# @return [
|
|
171
|
+
# @return [Hash] independent arguments with spacing and offsets normalized to SVG numbers
|
|
167
172
|
# @raise [Sevgi::ArgumentError] when an argument is missing or invalid
|
|
168
|
-
def
|
|
169
|
-
kwargs.
|
|
170
|
-
|
|
173
|
+
def normalize(**kwargs)
|
|
174
|
+
kwargs.to_h do |name, value|
|
|
175
|
+
ArgumentError.("Argument '#{name}' required") if value == Undefined
|
|
176
|
+
[name, normalize_value(name, value)]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
171
179
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
180
|
+
def coordinates(**coordinates)
|
|
181
|
+
coordinates.reject { |_, value| value.zero? }
|
|
182
|
+
end
|
|
176
183
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
def normalize_value(name, value)
|
|
185
|
+
return number(name, value) if FINITE.include?(name)
|
|
186
|
+
return value unless (assertion = ASSERTION[name])
|
|
187
|
+
return value unless (issue = assertion.call(name, value))
|
|
188
|
+
|
|
189
|
+
ArgumentError.(issue)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def number(name, value)
|
|
193
|
+
Scalar.number(value, context: "tile", field: name)
|
|
194
|
+
rescue ::Sevgi::ArgumentError
|
|
195
|
+
ArgumentError.("Argument '#{name}' must be a finite real number")
|
|
180
196
|
end
|
|
181
197
|
|
|
182
198
|
# Returns positional tile CSS classes.
|
|
@@ -191,6 +207,8 @@ module Sevgi
|
|
|
191
207
|
classes << "#{PREFIX}-#{as}-last" if index + 1 == upper
|
|
192
208
|
end
|
|
193
209
|
end
|
|
210
|
+
|
|
211
|
+
private :normalize_value, :number
|
|
194
212
|
end
|
|
195
213
|
|
|
196
214
|
private_constant :Helper
|
|
@@ -4,29 +4,57 @@ 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
|
|
14
|
+
# Keep box validation here: mixture helper methods would also enter the SVG DSL namespace.
|
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
16
|
+
|
|
8
17
|
# Aligns an inner box inside an outer box.
|
|
9
18
|
# @param position [Symbol, String, nil] alignment name
|
|
10
|
-
# @param inner [#width, #height, nil] inner box
|
|
11
|
-
# @param outer [#width, #height, nil] outer box
|
|
19
|
+
# @param inner [#width, #height, nil] inner box, with optional position exposing x and y
|
|
20
|
+
# @param outer [#width, #height, nil] outer box, with optional position exposing x and y
|
|
12
21
|
# @return [Sevgi::Graphics::Element] self
|
|
13
22
|
# @raise [Sevgi::ArgumentError] when alignment is unsupported
|
|
23
|
+
# @raise [Sevgi::ArgumentError] when a box dimension is not a finite real number
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when a supplied box position does not expose finite real x and y coordinates
|
|
25
|
+
# @note Size-only boxes have origin (0, 0). Positions describe known box geometry, not renderer-computed bounds.
|
|
14
26
|
def Align(position, inner:, outer:)
|
|
15
27
|
return self unless position && inner && outer
|
|
16
28
|
|
|
17
29
|
case position.to_sym
|
|
18
30
|
when :center
|
|
19
|
-
|
|
31
|
+
dimensions = [inner.width, inner.height, outer.width, outer.height]
|
|
32
|
+
iw, ih, ow, oh = dimensions.map { Scalar.number(it, context: "alignment", field: :dimension) }
|
|
33
|
+
origins = [inner, outer].map do |box|
|
|
34
|
+
next [0, 0] unless box.respond_to?(:position)
|
|
35
|
+
|
|
36
|
+
origin = box.position
|
|
37
|
+
unless origin.respond_to?(:x) && origin.respond_to?(:y)
|
|
38
|
+
ArgumentError.("Alignment box position must expose x and y")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
[origin.x, origin.y].map { Scalar.number(it, context: "alignment", field: :position) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
(ix, iy), (ox, oy) = origins
|
|
45
|
+
Translate(ox - ix + ((ow - iw) / 2.0), oy - iy + ((oh - ih) / 2.0))
|
|
20
46
|
else
|
|
21
47
|
ArgumentError.("Unsupported alignment: #{position}")
|
|
22
48
|
end
|
|
23
49
|
end
|
|
24
50
|
|
|
51
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
52
|
+
|
|
25
53
|
# Appends a scale(-1, -1) transform.
|
|
26
54
|
# @return [Sevgi::Graphics::Element] self
|
|
27
55
|
def Flip
|
|
28
56
|
tap do
|
|
29
|
-
attributes[:"transform#{
|
|
57
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, -1)"
|
|
30
58
|
end
|
|
31
59
|
end
|
|
32
60
|
|
|
@@ -34,7 +62,7 @@ module Sevgi
|
|
|
34
62
|
# @return [Sevgi::Graphics::Element] self
|
|
35
63
|
def FlipX
|
|
36
64
|
tap do
|
|
37
|
-
attributes[:"transform#{
|
|
65
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(-1, 1)"
|
|
38
66
|
end
|
|
39
67
|
end
|
|
40
68
|
|
|
@@ -42,36 +70,40 @@ module Sevgi
|
|
|
42
70
|
# @return [Sevgi::Graphics::Element] self
|
|
43
71
|
def FlipY
|
|
44
72
|
tap do
|
|
45
|
-
attributes[:"transform#{
|
|
73
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(1, -1)"
|
|
46
74
|
end
|
|
47
75
|
end
|
|
48
76
|
|
|
49
77
|
# Appends a six-value SVG matrix transform.
|
|
50
|
-
# @param values [Array<Numeric>] matrix values
|
|
78
|
+
# @param values [Array<Numeric>] finite matrix values, normalized to SVG numbers
|
|
51
79
|
# @return [Sevgi::Graphics::Element] self
|
|
52
|
-
# @raise [Sevgi::ArgumentError] when
|
|
80
|
+
# @raise [Sevgi::ArgumentError] when six finite real values are not supplied
|
|
53
81
|
def Matrix(*values)
|
|
54
82
|
tap do
|
|
55
83
|
ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
|
|
84
|
+
values = Scalar.numbers(values, context: "matrix")
|
|
56
85
|
|
|
57
|
-
attributes[:"transform#{
|
|
86
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "matrix(#{values.join(" ")})"
|
|
58
87
|
end
|
|
59
88
|
end
|
|
60
89
|
|
|
61
90
|
# Appends an SVG rotate transform.
|
|
62
|
-
# @param a [Numeric] angle in degrees
|
|
63
|
-
# @param origin [Array<Numeric>] optional x and y origin
|
|
91
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
92
|
+
# @param origin [Array<Numeric>] optional finite x and y origin, normalized to SVG numbers
|
|
64
93
|
# @return [Sevgi::Graphics::Element] self
|
|
65
|
-
# @raise [Sevgi::ArgumentError] when origin is not two coordinates
|
|
94
|
+
# @raise [Sevgi::ArgumentError] when angle or origin is not finite real, or origin is not two coordinates
|
|
66
95
|
def Rotate(a, *origin)
|
|
67
96
|
tap do
|
|
68
97
|
if !origin.empty? && origin.size != 2
|
|
69
98
|
ArgumentError.("Incorrect origin (two coordinates required): #{origin}")
|
|
70
99
|
end
|
|
71
100
|
|
|
72
|
-
|
|
101
|
+
angle = Scalar.number(a, context: "rotation", field: :angle)
|
|
102
|
+
origin = Scalar.numbers(origin, context: "rotation")
|
|
73
103
|
|
|
74
|
-
|
|
104
|
+
next if angle.zero?
|
|
105
|
+
|
|
106
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "rotate(#{[angle, *origin].join(", ")})"
|
|
75
107
|
end
|
|
76
108
|
end
|
|
77
109
|
|
|
@@ -88,56 +120,83 @@ module Sevgi
|
|
|
88
120
|
def RotateLeft(...) = Rotate(-90, ...)
|
|
89
121
|
|
|
90
122
|
# 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
|
|
123
|
+
# @param x [Numeric] finite x scale, or uniform scale when y is nil
|
|
124
|
+
# @param y [Numeric, nil] finite y scale
|
|
93
125
|
# @return [Sevgi::Graphics::Element] self
|
|
126
|
+
# @raise [Sevgi::ArgumentError] when a scale is not a finite real number
|
|
94
127
|
def Scale(x, y = nil)
|
|
95
128
|
tap do
|
|
96
|
-
|
|
129
|
+
x = Scalar.number(x, context: "scale", field: :x)
|
|
130
|
+
y = Scalar.number(y, context: "scale", field: :y) unless y.nil?
|
|
131
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "scale(#{(y.nil? ? [x] : [x, y]).join(", ")})"
|
|
97
132
|
end
|
|
98
133
|
end
|
|
99
134
|
|
|
100
135
|
# 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
|
|
136
|
+
# @param ax [Numeric] finite x skew angle in degrees, normalized before calculation
|
|
137
|
+
# @param ay [Numeric] finite y skew angle in degrees, normalized before calculation
|
|
103
138
|
# @return [Sevgi::Graphics::Element] self
|
|
139
|
+
# @raise [Sevgi::ArgumentError] when an angle is not a finite real number
|
|
104
140
|
def Skew(ax, ay)
|
|
141
|
+
ax = Scalar.number(ax, context: "skew", field: :x)
|
|
142
|
+
ay = Scalar.number(ay, context: "skew", field: :y)
|
|
105
143
|
Matrix(1.0, ::Math.tan(ay / 180.0 * ::Math::PI), ::Math.tan(ax / 180.0 * ::Math::PI), 1.0, 0.0, 0.0)
|
|
106
144
|
end
|
|
107
145
|
|
|
108
146
|
# Appends an SVG skewX transform.
|
|
109
|
-
# @param a [Numeric] angle in degrees
|
|
147
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
110
148
|
# @return [Sevgi::Graphics::Element] self
|
|
149
|
+
# @raise [Sevgi::ArgumentError] when angle is not a finite real number
|
|
111
150
|
def SkewX(a)
|
|
112
151
|
tap do
|
|
113
|
-
|
|
152
|
+
angle = Scalar.number(a, context: "skew", field: :x)
|
|
153
|
+
next if angle.zero?
|
|
114
154
|
|
|
115
|
-
attributes[:"transform#{
|
|
155
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewX(#{angle})"
|
|
116
156
|
end
|
|
117
157
|
end
|
|
118
158
|
|
|
119
159
|
# Appends an SVG skewY transform.
|
|
120
|
-
# @param a [Numeric] angle in degrees
|
|
160
|
+
# @param a [Numeric] finite angle in degrees, normalized to an SVG number
|
|
121
161
|
# @return [Sevgi::Graphics::Element] self
|
|
162
|
+
# @raise [Sevgi::ArgumentError] when angle is not a finite real number
|
|
122
163
|
def SkewY(a)
|
|
123
164
|
tap do
|
|
124
|
-
|
|
165
|
+
angle = Scalar.number(a, context: "skew", field: :y)
|
|
166
|
+
next if angle.zero?
|
|
125
167
|
|
|
126
|
-
attributes[:"transform#{
|
|
168
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "skewY(#{angle})"
|
|
127
169
|
end
|
|
128
170
|
end
|
|
129
171
|
|
|
130
|
-
# Appends an SVG translate transform.
|
|
131
|
-
# @param x [Numeric] x translation
|
|
132
|
-
# @param y [Numeric, nil] y translation
|
|
172
|
+
# Appends an SVG translate transform. One argument translates only the x axis.
|
|
173
|
+
# @param x [Numeric] finite x translation, normalized to an SVG number
|
|
174
|
+
# @param y [Numeric, nil] finite y translation, normalized to an SVG number
|
|
133
175
|
# @return [Sevgi::Graphics::Element] self
|
|
176
|
+
# @raise [Sevgi::ArgumentError] when a coordinate is not a finite real number
|
|
134
177
|
def Translate(x, y = nil)
|
|
135
178
|
tap do
|
|
136
|
-
|
|
179
|
+
dx = Scalar.number(x, context: "translation", field: :x)
|
|
180
|
+
dy = Scalar.number(y, context: "translation", field: :y) unless y.nil?
|
|
181
|
+
next if dx.zero? && (dy.nil? || dy.zero?)
|
|
137
182
|
|
|
138
|
-
attributes[:"transform#{
|
|
183
|
+
attributes[:"transform#{Attributes::UPDATE_SUFFIX}"] = "translate(#{(dy.nil? ? [dx] : [dx, dy]).join(" ")})"
|
|
139
184
|
end
|
|
140
185
|
end
|
|
186
|
+
|
|
187
|
+
# Appends an x-axis SVG translate transform.
|
|
188
|
+
# @param x [Numeric] finite x translation
|
|
189
|
+
# @return [Sevgi::Graphics::Element] self
|
|
190
|
+
# @raise [Sevgi::ArgumentError] when x is not a finite real number
|
|
191
|
+
# @see #Translate
|
|
192
|
+
def TranslateX(x) = Translate(x)
|
|
193
|
+
|
|
194
|
+
# Appends a y-axis SVG translate transform.
|
|
195
|
+
# @param y [Numeric] finite y translation
|
|
196
|
+
# @return [Sevgi::Graphics::Element] self
|
|
197
|
+
# @raise [Sevgi::ArgumentError] when y is not a finite real number
|
|
198
|
+
# @see #Translate
|
|
199
|
+
def TranslateY(y) = Translate(0, y)
|
|
141
200
|
end
|
|
142
201
|
end
|
|
143
202
|
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
|
|
@@ -15,7 +18,7 @@ module Sevgi
|
|
|
15
18
|
# Adds an XML comment.
|
|
16
19
|
# @param comment [Object] comment text
|
|
17
20
|
# @return [Sevgi::Graphics::Element] floating comment element
|
|
18
|
-
# @raise [Sevgi::ArgumentError] when comment cannot be stringified as valid XML or
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when comment cannot be stringified as valid XML or forms malformed markup
|
|
19
22
|
def Comment(comment)
|
|
20
23
|
comment = XML.text(comment, context: "XML comment")
|
|
21
24
|
|
|
@@ -25,16 +28,22 @@ module Sevgi
|
|
|
25
28
|
_(Content.verbatim("<!-- #{comment} -->"))
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
# Merges
|
|
29
|
-
# Only the direct root-to-self ancestor chain participates
|
|
30
|
-
# present on multiple chain elements, the nearest element to the receiver wins.
|
|
31
|
-
#
|
|
31
|
+
# Merges `-context` metadata from the document root, ancestors, and this element.
|
|
32
|
+
# Only the direct root-to-self ancestor chain participates. Sibling subtrees are ignored. When the same key is
|
|
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
|
)
|