sevgi 0.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 +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +674 -0
- data/README.md +15 -0
- data/bin/sevgi +136 -0
- data/lib/sevgi/errors.rb +11 -0
- data/lib/sevgi/external.rb +8 -0
- data/lib/sevgi/function.rb +35 -0
- data/lib/sevgi/geometry/elements/rect.rb +133 -0
- data/lib/sevgi/geometry/elements/segment.rb +102 -0
- data/lib/sevgi/geometry/elements.rb +41 -0
- data/lib/sevgi/geometry/equation/line/diagonal.rb +90 -0
- data/lib/sevgi/geometry/equation/line/horizontal.rb +15 -0
- data/lib/sevgi/geometry/equation/line/vertical.rb +52 -0
- data/lib/sevgi/geometry/equation/line.rb +38 -0
- data/lib/sevgi/geometry/equation.rb +3 -0
- data/lib/sevgi/geometry/errors.rb +5 -0
- data/lib/sevgi/geometry/external.rb +15 -0
- data/lib/sevgi/geometry/operation/align.rb +40 -0
- data/lib/sevgi/geometry/operation/sweep.rb +52 -0
- data/lib/sevgi/geometry/operation.rb +28 -0
- data/lib/sevgi/geometry/point.rb +62 -0
- data/lib/sevgi/geometry.rb +10 -0
- data/lib/sevgi/graphics/attribute.rb +125 -0
- data/lib/sevgi/graphics/canvas.rb +51 -0
- data/lib/sevgi/graphics/content.rb +72 -0
- data/lib/sevgi/graphics/document/base.rb +51 -0
- data/lib/sevgi/graphics/document/default.rb +19 -0
- data/lib/sevgi/graphics/document/html.rb +11 -0
- data/lib/sevgi/graphics/document/inkscape.rb +17 -0
- data/lib/sevgi/graphics/document/minimal.rb +11 -0
- data/lib/sevgi/graphics/document.rb +53 -0
- data/lib/sevgi/graphics/element.rb +85 -0
- data/lib/sevgi/graphics/external.rb +15 -0
- data/lib/sevgi/graphics/mixtures/core.rb +81 -0
- data/lib/sevgi/graphics/mixtures/duplicate.rb +36 -0
- data/lib/sevgi/graphics/mixtures/hatch.rb +21 -0
- data/lib/sevgi/graphics/mixtures/identify.rb +93 -0
- data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
- data/lib/sevgi/graphics/mixtures/lint.rb +33 -0
- data/lib/sevgi/graphics/mixtures/render.rb +149 -0
- data/lib/sevgi/graphics/mixtures/replicate.rb +125 -0
- data/lib/sevgi/graphics/mixtures/save.rb +23 -0
- data/lib/sevgi/graphics/mixtures/transform.rb +78 -0
- data/lib/sevgi/graphics/mixtures/underscore.rb +21 -0
- data/lib/sevgi/graphics/mixtures/validate.rb +27 -0
- data/lib/sevgi/graphics/mixtures/wrappers.rb +63 -0
- data/lib/sevgi/graphics/mixtures.rb +16 -0
- data/lib/sevgi/graphics.rb +10 -0
- data/lib/sevgi/internal/constant.rb +5 -0
- data/lib/sevgi/internal/dim.rb +13 -0
- data/lib/sevgi/internal/function/file.rb +30 -0
- data/lib/sevgi/internal/function/float.rb +35 -0
- data/lib/sevgi/internal/function/formula.rb +71 -0
- data/lib/sevgi/internal/function/math.rb +43 -0
- data/lib/sevgi/internal/function/string.rb +13 -0
- data/lib/sevgi/internal/function.rb +7 -0
- data/lib/sevgi/internal/list.rb +27 -0
- data/lib/sevgi/internal/locate.rb +47 -0
- data/lib/sevgi/internal/margin.rb +23 -0
- data/lib/sevgi/internal/minitest/script.rb +50 -0
- data/lib/sevgi/internal/minitest/shell.rb +71 -0
- data/lib/sevgi/internal/minitest/suite.rb +35 -0
- data/lib/sevgi/internal/minitest.rb +5 -0
- data/lib/sevgi/internal/paper.rb +26 -0
- data/lib/sevgi/internal.rb +10 -0
- data/lib/sevgi/standard/conform.rb +55 -0
- data/lib/sevgi/standard/data/attribute.rb +496 -0
- data/lib/sevgi/standard/data/element.rb +269 -0
- data/lib/sevgi/standard/data/specification.rb +1841 -0
- data/lib/sevgi/standard/data.rb +79 -0
- data/lib/sevgi/standard/errors.rb +28 -0
- data/lib/sevgi/standard/model.rb +82 -0
- data/lib/sevgi/standard.rb +26 -0
- data/lib/sevgi/utensils/external.rb +22 -0
- data/lib/sevgi/utensils/grid.rb +49 -0
- data/lib/sevgi/utensils/ruler.rb +47 -0
- data/lib/sevgi/utensils/tsquare.rb +28 -0
- data/lib/sevgi/utensils.rb +7 -0
- data/lib/sevgi/version.rb +5 -0
- data/lib/sevgi.rb +10 -0
- metadata +127 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
module Operation
|
6
|
+
module Align
|
7
|
+
def align(element, other, alignment = :center)
|
8
|
+
translate(element, other, alignment).apply(element)
|
9
|
+
end
|
10
|
+
|
11
|
+
def alignment(element, other, alignment = :center)
|
12
|
+
this, that = element.bbox, other.bbox
|
13
|
+
|
14
|
+
case alignment
|
15
|
+
when :center then Translation[(this.width - that.width) / 2.0, (this.height - that.height) / 2.0]
|
16
|
+
when :left then Translation[this.ne.x - that.ne.x, 0]
|
17
|
+
when :right then Translation[this.sw.x - that.sw.x, 0]
|
18
|
+
when :top then Translation[0, this.ne.y - that.ne.y]
|
19
|
+
when :bottom then Translation[0, this.sw.y - that.sw.y]
|
20
|
+
else ArgumentError.("No such type of alignment: #{alignment}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def applicable?(element)
|
25
|
+
Translation.applicable?(element)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
register(Align, :align, :alignment)
|
30
|
+
end
|
31
|
+
|
32
|
+
Translation = Data.define(:dx, :dy) do
|
33
|
+
def apply(element) = element.translate(**to_h)
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def applicable?(element) = element.respond_to?(:translate)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
module Operation
|
6
|
+
module Sweep
|
7
|
+
extend self
|
8
|
+
|
9
|
+
LIMIT = 1_000
|
10
|
+
|
11
|
+
def sweep(element, initial:, direction:, step:, limit: LIMIT, &block)
|
12
|
+
line = Equation::Line.from_direction(point: initial, direction:)
|
13
|
+
|
14
|
+
[
|
15
|
+
*unisweep(element, line.shift(-step), -step, limit:).reverse,
|
16
|
+
*unisweep(element, line, step, limit:),
|
17
|
+
].tap do |segments|
|
18
|
+
yield(segments) if block
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def sweep!(element, initial:, direction:, step:, limit: LIMIT, &block)
|
23
|
+
sweep(element, initial: initial, direction:, step:, limit:) do |segments|
|
24
|
+
OperationError.("No segments found [initial: #{initial}, direction: #{direction} step: #{step}]") if segments.empty?
|
25
|
+
|
26
|
+
yield(segments) if block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def unisweep(element, line, step, limit: LIMIT)
|
31
|
+
segments = []
|
32
|
+
|
33
|
+
limit.times do
|
34
|
+
return segments unless (segment = element.intersection(line))
|
35
|
+
|
36
|
+
segments << segment unless segment.ignorable?
|
37
|
+
|
38
|
+
line = line.shift(step)
|
39
|
+
end
|
40
|
+
|
41
|
+
OperationError.("Loop limit reached: #{limit}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def applicable?(element)
|
45
|
+
element.respond_to?(:intersection)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
register(Sweep, :sweep, :sweep!, :unisweep)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
module Operation
|
6
|
+
extend self
|
7
|
+
|
8
|
+
OperationError = Class.new(GeometryError)
|
9
|
+
OperationInapplicableError = Class.new(GeometryError)
|
10
|
+
|
11
|
+
def register(handler, *operations) = operations.each { |operation| def_operation(operation, handler) }
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def def_operation(operation, handler)
|
16
|
+
define_singleton_method(operation) do |element, *args, **kwargs, &block|
|
17
|
+
OperationInapplicableError.("Not a Geometric Element: #{element}") unless element.is_a?(Geometry::Element)
|
18
|
+
OperationInapplicableError.("Unapplicable operation for #{element}: #{handler}") unless handler.applicable?(element)
|
19
|
+
|
20
|
+
handler.public_send(operation, element, *args, **kwargs, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require_relative "operation/align"
|
28
|
+
require_relative "operation/sweep"
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
Point = Data.define(:x, :y) do
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
def initialize(x:, y:)
|
9
|
+
super(x: x.to_f, y: y.to_f)
|
10
|
+
end
|
11
|
+
|
12
|
+
def above?(other)
|
13
|
+
y <= other.y
|
14
|
+
end
|
15
|
+
|
16
|
+
def below?(other)
|
17
|
+
y >= other.y
|
18
|
+
end
|
19
|
+
|
20
|
+
def approx(precision = nil)
|
21
|
+
with(x: F.approx(x, precision), y: F.approx(y, precision))
|
22
|
+
end
|
23
|
+
|
24
|
+
def eq?(other, precision: nil)
|
25
|
+
self.class.eq?(self, other, precision:)
|
26
|
+
end
|
27
|
+
|
28
|
+
def infinite?
|
29
|
+
[x, y].any?(&:infinite?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def nan?
|
33
|
+
[x, y].any?(&:nan?)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"P(#{F.approx(x)} #{F.approx(y)})"
|
38
|
+
end
|
39
|
+
|
40
|
+
def translate(dx: nil, dy: nil)
|
41
|
+
with(x: x + (dx || 0.0), y: y + (dy || 0.0))
|
42
|
+
end
|
43
|
+
|
44
|
+
def unordered_between?(p, q)
|
45
|
+
nan? ? false : between?([p, q].min, [p, q].max)
|
46
|
+
end
|
47
|
+
|
48
|
+
def <=>(other)
|
49
|
+
return unless other.is_a?(self.class)
|
50
|
+
return if other.nan? || nan?
|
51
|
+
|
52
|
+
deconstruct <=> other.deconstruct
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def eq?(p, q, precision: nil) = F.eq?(p.x, q.x, precision:) && F.eq?(p.y, q.y, precision:)
|
57
|
+
|
58
|
+
def origin = new(x: 0.0, y: 0.0)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "geometry/errors"
|
4
|
+
|
5
|
+
require_relative "geometry/point"
|
6
|
+
require_relative "geometry/equation"
|
7
|
+
require_relative "geometry/elements"
|
8
|
+
require_relative "geometry/operation"
|
9
|
+
|
10
|
+
require_relative "geometry/external"
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Some parts are adapted from https://github.com/DannyBen/victor (lib/victor/attributes.rb)
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Graphics
|
7
|
+
module Attribute
|
8
|
+
module Ident
|
9
|
+
INTERNAL_PREFIX = "_"
|
10
|
+
UPDATE_SUFFIX = "+"
|
11
|
+
|
12
|
+
def internal?(given)
|
13
|
+
(@internal ||= {})[given] ||= given.start_with?(INTERNAL_PREFIX)
|
14
|
+
end
|
15
|
+
|
16
|
+
def id(given)
|
17
|
+
(@id ||= {})[given] ||= (updateable?(given) ? given.to_s.delete_suffix(UPDATE_SUFFIX) : given).to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def updateable?(given)
|
21
|
+
(@updateable ||= {})[given] ||= given.end_with?(UPDATE_SUFFIX)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
extend Ident
|
26
|
+
|
27
|
+
class Store
|
28
|
+
def initialize(attributes = {})
|
29
|
+
@store = {}
|
30
|
+
|
31
|
+
import(attributes)
|
32
|
+
end
|
33
|
+
|
34
|
+
def import(attributes)
|
35
|
+
hash = attributes.compact.to_a.map do |key, value|
|
36
|
+
[key.to_sym, value.is_a?(::Hash) ? value.transform_keys!(&:to_sym) : value]
|
37
|
+
end.to_h
|
38
|
+
|
39
|
+
@store.merge!(hash)
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](key)
|
43
|
+
@store[Attribute.id(key)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def []=(key, value)
|
47
|
+
return if value.nil?
|
48
|
+
|
49
|
+
@store[id = Attribute.id(key)] = @store.key?(id) && Attribute.updateable?(key) ? update(id, value) : value
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(key)
|
53
|
+
@store.delete(Attribute.id(key))
|
54
|
+
end
|
55
|
+
|
56
|
+
def export
|
57
|
+
hash = @store.reject { |id, _| Attribute.internal?(id) }
|
58
|
+
return hash unless hash.key?(:id)
|
59
|
+
|
60
|
+
# A small aesthetic touch: always keep the id attribute first
|
61
|
+
{ id: hash.delete(:id), **hash }
|
62
|
+
end
|
63
|
+
|
64
|
+
def has?(key)
|
65
|
+
@store.key?(Attribute.id(key))
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize_copy(original)
|
69
|
+
@store = {}
|
70
|
+
original.store.each { |key, value| @store[key] = value.dup }
|
71
|
+
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
def list
|
76
|
+
export.keys
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_h
|
80
|
+
@store
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_xml_lines
|
84
|
+
export.map { |id, value| to_xml(id, value) }
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
attr_reader :store
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
UPDATER = {
|
94
|
+
::String => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ") },
|
95
|
+
::Symbol => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ").to_sym },
|
96
|
+
::Array => proc { |old_value, new_value| [old_value, new_value] },
|
97
|
+
::Hash => proc { |old_value, new_value| merge(old_value, new_value.transform_keys(&:to_sym)) },
|
98
|
+
}.freeze
|
99
|
+
|
100
|
+
def update(id, new_value)
|
101
|
+
(old_value = @store[id]).nil? ? new_value : UPDATER[new_value.class].call(*sanitized(old_value, new_value))
|
102
|
+
end
|
103
|
+
|
104
|
+
def sanitized(old_value, new_value)
|
105
|
+
ArgumentError.("Incompatible values: #{new_value} vs #{old_value}") unless new_value.is_a?(old_value.class)
|
106
|
+
ArgumentError.("Unsupported value for update: #{new_value}") unless UPDATER.key?(new_value.class)
|
107
|
+
|
108
|
+
[old_value, new_value]
|
109
|
+
end
|
110
|
+
|
111
|
+
private_constant :UPDATER
|
112
|
+
|
113
|
+
def to_xml(id, value)
|
114
|
+
case value
|
115
|
+
when ::Hash then %[#{id}="#{value.map { "#{_1}:#{_2}" }.join("; ")}"]
|
116
|
+
when ::Array then %[#{id}="#{value.join(" ")}"]
|
117
|
+
else %[#{id}=#{value.to_s.encode(xml: :attr)}]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
Attributes = Attribute::Store
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Graphics
|
7
|
+
class Canvas
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@margin, *Margin.members
|
10
|
+
def_delegators :@dim, *Dim.members
|
11
|
+
|
12
|
+
attr_reader :dim, :margin, :content
|
13
|
+
|
14
|
+
def initialize(width:, height:, unit: "mm", margins: [])
|
15
|
+
@dim = Dim[width, height, unit]
|
16
|
+
@margin = Margin[*margins]
|
17
|
+
|
18
|
+
compute
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes = { **viewport, viewBox: viewbox }
|
23
|
+
|
24
|
+
def rect = content.rect
|
25
|
+
|
26
|
+
def viewport = { width: "#{width}#{unit}", height: "#{height}#{unit}" }
|
27
|
+
|
28
|
+
def viewbox = F.prettify(-margin.left, -margin.top, width, height).join(" ")
|
29
|
+
|
30
|
+
def with(*margins) = margins.empty? ? self : self.class.new(**dim.to_h, margins:)
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def compute
|
35
|
+
@content = dim.with(width: width - margin.left - margin.right, height: height - margin.top - margin.bottom)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def call(arg = Paper.default, *)
|
40
|
+
case arg
|
41
|
+
when Canvas then arg
|
42
|
+
when Symbol then new(**Paper[arg].to_h)
|
43
|
+
else ArgumentError.("Argument must be a Symbol or Canvas instance: #{arg}")
|
44
|
+
end => canvas
|
45
|
+
|
46
|
+
canvas.with(*)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
class Content
|
6
|
+
attr_reader :content
|
7
|
+
|
8
|
+
def initialize(content) = @content = content
|
9
|
+
|
10
|
+
def render(renderer, depth) = raise NotImplementedError
|
11
|
+
|
12
|
+
def to_s = content.to_s
|
13
|
+
|
14
|
+
class CData < Content
|
15
|
+
def render(renderer, depth)
|
16
|
+
depth += 1
|
17
|
+
|
18
|
+
renderer.append(depth, "<![CDATA[")
|
19
|
+
renderer.append(depth + 1, *Array(content))
|
20
|
+
renderer.append(depth, "]]>")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class CSS < Content
|
25
|
+
def initialize(content)
|
26
|
+
ArgumentError.("CSS content must be a hash: #{content}") unless content.is_a?(::Hash)
|
27
|
+
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def render(renderer, depth)
|
32
|
+
depth += 1
|
33
|
+
|
34
|
+
renderer.append(depth, "<![CDATA[")
|
35
|
+
|
36
|
+
depth += 1
|
37
|
+
content.each do |rule, styles|
|
38
|
+
case styles
|
39
|
+
when ::Hash
|
40
|
+
renderer.append(depth, "#{rule} {")
|
41
|
+
renderer.append(depth + 1, *styles.map { |key, value| "#{key}: #{value};" })
|
42
|
+
renderer.append(depth, "}")
|
43
|
+
when ::String, ::Symbol, ::Numeric
|
44
|
+
renderer.append(depth, "#{rule}: #{styles};")
|
45
|
+
else
|
46
|
+
ArgumentError.("Malformed style: #{styles}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
depth -= 1
|
50
|
+
|
51
|
+
renderer.append(depth, "]]>")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Encoded < Content
|
56
|
+
def to_s = content.to_s.encode(xml: :text)
|
57
|
+
|
58
|
+
def render(renderer, depth) = renderer.append(depth + 1, to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
class Verbatim < Content
|
62
|
+
def render(renderer, depth) = renderer.append(depth + 1, to_s)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
extend self
|
67
|
+
|
68
|
+
def Content(arg) = Content::Encoded.new(arg)
|
69
|
+
|
70
|
+
def Text(contents) = Array(contents).map(&:to_s).join("\n")
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Document
|
6
|
+
DEFAULTS = { lint: true, validate: true }.freeze
|
7
|
+
|
8
|
+
class Proto < Element
|
9
|
+
extend Profile::DSL
|
10
|
+
|
11
|
+
mixture Mixtures::Core
|
12
|
+
mixture Mixtures::Render
|
13
|
+
|
14
|
+
def call(*, **)
|
15
|
+
options = DEFAULTS.merge(**)
|
16
|
+
|
17
|
+
self.Process(*, **options) if respond_to?(:Process)
|
18
|
+
self.Render(*, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def attributes = self == Proto ? {} : { **superclass.attributes, **profile.attributes }
|
23
|
+
|
24
|
+
def preambles = self == Proto ? nil : profile.preambles || superclass.preambles
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Base < Proto
|
29
|
+
public_class_method :new
|
30
|
+
|
31
|
+
document :base
|
32
|
+
|
33
|
+
mixture Mixtures::Duplicate
|
34
|
+
mixture Mixtures::Hatch
|
35
|
+
mixture Mixtures::Identify
|
36
|
+
mixture Mixtures::Lint
|
37
|
+
mixture Mixtures::Replicate
|
38
|
+
mixture Mixtures::Save
|
39
|
+
mixture Mixtures::Transform
|
40
|
+
mixture Mixtures::Underscore
|
41
|
+
mixture Mixtures::Validate
|
42
|
+
mixture Mixtures::Wrappers
|
43
|
+
|
44
|
+
def Process(**options)
|
45
|
+
self.Validate if options[:validate]
|
46
|
+
self.Lint if options[:lint]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Document
|
6
|
+
class Default < Minimal
|
7
|
+
document :default,
|
8
|
+
attributes: {
|
9
|
+
"xmlns": "http://www.w3.org/2000/svg",
|
10
|
+
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
11
|
+
},
|
12
|
+
preambles: [
|
13
|
+
'<?xml version="1.0" standalone="no"?>',
|
14
|
+
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Document
|
6
|
+
class Inkscape < Default
|
7
|
+
document :inkscape, attributes: {
|
8
|
+
"xmlns:inkscape": "http://www.inkscape.org/slugs/inkscape",
|
9
|
+
"xmlns:sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
|
10
|
+
"shape-rendering": "crispEdges",
|
11
|
+
}
|
12
|
+
|
13
|
+
mixture Mixtures::Inkscape
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
class Profile
|
6
|
+
@available = {}
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :available
|
10
|
+
|
11
|
+
def call(document, canvas = nil, **, &block)
|
12
|
+
(klass = self[document]).root(**klass.attributes, **(canvas ? Graphics.Canvas(canvas).attributes : {}), **, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def register(name, klass) = (available[name] = klass)
|
16
|
+
|
17
|
+
def [](name) = available[name]
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :name, :attributes, :preambles
|
21
|
+
|
22
|
+
def initialize(name, attributes: nil, preambles: nil)
|
23
|
+
@name = name
|
24
|
+
@attributes = attributes || {}
|
25
|
+
@preambles = preambles
|
26
|
+
end
|
27
|
+
|
28
|
+
module DSL
|
29
|
+
attr_reader :profile
|
30
|
+
|
31
|
+
def document(name, attributes: {}, preambles: nil)
|
32
|
+
@profile = Profile.new(name, attributes:, preambles:).tap do
|
33
|
+
Profile.register(name, self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def mixture(*modules)
|
38
|
+
modules.each do |mod|
|
39
|
+
include(mod::InstanceMethods) if defined?(mod::InstanceMethods)
|
40
|
+
extend(mod::ClassMethods) if defined?(mod::ClassMethods)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
require_relative "document/base"
|
49
|
+
require_relative "document/minimal"
|
50
|
+
|
51
|
+
require_relative "document/default"
|
52
|
+
require_relative "document/html"
|
53
|
+
require_relative "document/inkscape"
|