sevgi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|