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,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sevgi/standard"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Graphics
|
7
|
+
class Element
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
RootParent = Object.new.tap { |this| def this.inspect = "RootParent" }.freeze
|
11
|
+
|
12
|
+
module Ident
|
13
|
+
def id(given) = (@id ||= {})[given] ||= given.to_s.tr("_", "-").to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
extend Ident
|
17
|
+
|
18
|
+
attr_reader :name, :attributes, :children, :contents, :parent
|
19
|
+
|
20
|
+
def initialize(name, attributes: {}, contents: [], parent:, &block)
|
21
|
+
@name = name
|
22
|
+
@attributes = Attributes.new(attributes)
|
23
|
+
@contents = contents
|
24
|
+
@parent = parent
|
25
|
+
@children = []
|
26
|
+
|
27
|
+
parent.children << self unless self.class.root?(self)
|
28
|
+
|
29
|
+
instance_exec(&block) if block
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(name, *, &block)
|
33
|
+
Element.valid?(id = Element.id(name)) ? Dispatch.(self, id, *, &block) : super
|
34
|
+
end
|
35
|
+
|
36
|
+
def respond_to_missing?(name, include_private = false)
|
37
|
+
Element.valid?(Element.id(name)) || super
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
attr_writer :children, :attributes
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def element(name, *, parent:, &block) = new(name, **Dispatch.parse(name, *), parent:, &block)
|
46
|
+
|
47
|
+
def root(*, &block) = element(:svg, *, parent: RootParent, &block)
|
48
|
+
|
49
|
+
def root?(element) = element.parent == RootParent
|
50
|
+
|
51
|
+
def valid?(name) = Standard.element?(name)
|
52
|
+
end
|
53
|
+
|
54
|
+
module Dispatch
|
55
|
+
extend self
|
56
|
+
|
57
|
+
def call(element, name, *, &block)
|
58
|
+
# Low-hanging fruit optimization: define missing method to avoid dispatching cost
|
59
|
+
Element.class_exec do
|
60
|
+
define_method(name) { |*args, &block| self.class.element(name, *args, parent: self, &block) }
|
61
|
+
end unless Element.method_defined?(name)
|
62
|
+
|
63
|
+
element.public_send(name, *, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse(name, *args)
|
67
|
+
attributes, contents = {}, []
|
68
|
+
|
69
|
+
args.each do |arg|
|
70
|
+
case arg
|
71
|
+
when ::Hash then attributes = arg
|
72
|
+
when ::String then contents << Graphics.Content(arg)
|
73
|
+
when Content then contents << arg
|
74
|
+
else ArgumentError.("Argument of element '#{name}' must be a Hash or String: #{arg}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
{ attributes:, contents: }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private_constant :Dispatch
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module External
|
6
|
+
def Canvas(...) = Canvas.(...)
|
7
|
+
|
8
|
+
def Verbatim(content) = Content::Verbatim.new(content)
|
9
|
+
|
10
|
+
def SVG(document = :default, canvas = nil, **, &block) = Profile.(document, canvas, **, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
extend External
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Graphics
|
7
|
+
module Mixtures
|
8
|
+
module Core
|
9
|
+
module InstanceMethods
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@attributes, :[], :[]=, :has?
|
12
|
+
def_delegators :@children, :first, :last, :at
|
13
|
+
|
14
|
+
def Adopt(new_parent = nil, index: -1)
|
15
|
+
tap do
|
16
|
+
if new_parent
|
17
|
+
ArgumentError.("Element type does not match the new parent type: #{self.class}") unless instance_of?(new_parent.class)
|
18
|
+
else
|
19
|
+
new_parent = parent
|
20
|
+
end
|
21
|
+
|
22
|
+
self.Orphan
|
23
|
+
(@parent = new_parent).children.insert(index, self)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def AdoptFirst(*)
|
28
|
+
Adopt(*, index: 0)
|
29
|
+
end
|
30
|
+
|
31
|
+
def Append(*elements)
|
32
|
+
tap { elements.each { _1.Adopt(self) } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def Orphan
|
36
|
+
parent.children&.delete(self) unless Root?
|
37
|
+
end
|
38
|
+
|
39
|
+
def Prepend(*elements)
|
40
|
+
tap { elements.each { _1.AdoptFirst(self) } }
|
41
|
+
end
|
42
|
+
|
43
|
+
def Root
|
44
|
+
element = self
|
45
|
+
while element
|
46
|
+
break if element.Root?
|
47
|
+
|
48
|
+
element = element.parent
|
49
|
+
end
|
50
|
+
element
|
51
|
+
end
|
52
|
+
|
53
|
+
def Root?
|
54
|
+
self.class.root?(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def Traverse(depth = 0, leave = nil, &block)
|
58
|
+
tap do
|
59
|
+
yield(self, depth)
|
60
|
+
|
61
|
+
children.each { |child| child.Traverse(depth + 1, leave, &block) }
|
62
|
+
leave&.call(self, depth)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def With(element = nil, ...)
|
67
|
+
tap { (element || self).parent.instance_exec(self, ...) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def Within(element = nil, ...)
|
71
|
+
tap { (element || self).instance_exec(...) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def <<(element)
|
75
|
+
Append(element)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Duplicate
|
7
|
+
module InstanceMethods
|
8
|
+
def Duplicate(dx: nil, dy: nil, parent: nil, &block)
|
9
|
+
duplicated = dup
|
10
|
+
|
11
|
+
duplicated.Traverse do |element|
|
12
|
+
element.children = element.children.map(&:dup)
|
13
|
+
id = (element.attributes = element.attributes.dup).delete(:id)
|
14
|
+
element[:_id] = id if id
|
15
|
+
block&.call(element)
|
16
|
+
end
|
17
|
+
|
18
|
+
duplicated.Translate(dx, dy) if dx || dy
|
19
|
+
|
20
|
+
duplicated.Adopt(parent)
|
21
|
+
|
22
|
+
duplicated
|
23
|
+
end
|
24
|
+
|
25
|
+
def DuplicateH(dx, parent: nil, &block)
|
26
|
+
Duplicate(dx:, dy: 0, parent:, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def DuplicateV(dy, parent: nil, &block)
|
30
|
+
Duplicate(dx: 0, dy:, parent:, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Hatch
|
7
|
+
module InstanceMethods
|
8
|
+
def Draw(segments, **)
|
9
|
+
segments.each { |segment| segment.draw(self, **) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def Hatch(canvas, direction:, step:, **)
|
13
|
+
Geometry::Operation.sweep!(rect = canvas.rect, initial: rect.position, direction:, step:).tap do |segments|
|
14
|
+
Draw(segments, **)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Identify
|
7
|
+
module InstanceMethods
|
8
|
+
class Identifiers
|
9
|
+
attr_reader :element, :namespace, :collision
|
10
|
+
|
11
|
+
def initialize(element)
|
12
|
+
@element = element
|
13
|
+
@namespace = {}
|
14
|
+
@collision = {}
|
15
|
+
|
16
|
+
build
|
17
|
+
end
|
18
|
+
|
19
|
+
def conflict?
|
20
|
+
!@collision.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](*)
|
24
|
+
@namespace[*]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build
|
30
|
+
element.Traverse do |element|
|
31
|
+
next unless (id = element[:id])
|
32
|
+
|
33
|
+
if @namespace.key?(id)
|
34
|
+
(@collision[id] ||= [@namespace[id]]) << element
|
35
|
+
else
|
36
|
+
@namespace[id] = element
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def Disidentify
|
43
|
+
Traverse do |element|
|
44
|
+
next unless element[:id]
|
45
|
+
|
46
|
+
element[:_id] = element.attributes.delete(:id)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def IdentifyAsTable(...) = IdentifyAsTable.(...)
|
51
|
+
|
52
|
+
def IdentifyAsList(...) = IdentifyAsList.(...)
|
53
|
+
|
54
|
+
def Reidentify
|
55
|
+
end
|
56
|
+
|
57
|
+
def Identifiers = Identifiers.new(self)
|
58
|
+
|
59
|
+
SEPARATOR = "-"
|
60
|
+
|
61
|
+
IdentifyAs = Data.define(:id) do
|
62
|
+
def label(*indexes) = (id and [id, *indexes].map(&:to_s).join(SEPARATOR))
|
63
|
+
end
|
64
|
+
|
65
|
+
IdentifyAsList = Class.new(IdentifyAs) do
|
66
|
+
class << self
|
67
|
+
def call(element, i)
|
68
|
+
element.each_with_index do |it, index|
|
69
|
+
i and (label = self[i].label(index + 1)) and it[:id] = label
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
IdentifyAsTable = Class.new(IdentifyAs) do
|
76
|
+
class << self
|
77
|
+
def call(element, ix: nil, iy: nil)
|
78
|
+
element.each_with_index do |row, irow|
|
79
|
+
iy and (label = self[iy].label(irow + 1)) and row[:id] = label
|
80
|
+
ix and row.children.each_with_index do |col, icol|
|
81
|
+
(label = self[ix].label(irow + 1, icol + 1)) and col[:id] = label
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private_constant :IdentifyAs, :IdentifyAsTable, :IdentifyAsList
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Inkscape
|
7
|
+
module InstanceMethods
|
8
|
+
def layer(**, &block)
|
9
|
+
g("inkscape:groupmode": "layer", "sodipodi:insensitive": "true", **, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
LintError = Class.new(Error)
|
5
|
+
|
6
|
+
module Graphics
|
7
|
+
module Mixtures
|
8
|
+
module Lint
|
9
|
+
module InstanceMethods
|
10
|
+
def Lint(...)
|
11
|
+
IdentitiesAreUniq.(self, ...)
|
12
|
+
end
|
13
|
+
|
14
|
+
module IdentitiesAreUniq
|
15
|
+
extend self
|
16
|
+
|
17
|
+
def call(element)
|
18
|
+
return true unless (identifiers = element.Identifiers).conflict?
|
19
|
+
|
20
|
+
collisions = identifiers.collision.map do |id, elements|
|
21
|
+
"Element(s) with the same id '#{id}': #{elements.map(&:name).uniq.join(", ")}"
|
22
|
+
end.map { "\t#{_1}" }.join("\n")
|
23
|
+
|
24
|
+
LintError.("Found Id collisions:\n#{collisions}")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private_constant :IdentitiesAreUniq
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Render
|
7
|
+
class Renderer
|
8
|
+
DEFAULTS = { indent: " ", linelength: 140, style: :hybrid }.freeze
|
9
|
+
|
10
|
+
module Attributes
|
11
|
+
module Block
|
12
|
+
def attributes(element, depth)
|
13
|
+
attributes_block(element, depth, element.attributes.to_xml_lines)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Hybrid
|
18
|
+
def attributes(element, depth)
|
19
|
+
if attributes_as_block?(lines = element.attributes.to_xml_lines, depth)
|
20
|
+
attributes_block(element, depth, lines)
|
21
|
+
else
|
22
|
+
attributes_inline(element, depth, lines)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def linelength(lines, depth)
|
27
|
+
indent(depth).length + lines.sum(&:length)
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_as_block?(lines, depth)
|
31
|
+
linelength(lines, depth) > options[:linelength]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Inline
|
36
|
+
def attributes(element, depth)
|
37
|
+
attributes_inline(element, depth, element.attributes.to_xml_lines)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private_constant :Attributes
|
43
|
+
|
44
|
+
attr_reader :root, :options, :output
|
45
|
+
|
46
|
+
def initialize(root, **)
|
47
|
+
@root = root
|
48
|
+
@options = DEFAULTS.merge(**)
|
49
|
+
@output = []
|
50
|
+
|
51
|
+
build
|
52
|
+
end
|
53
|
+
|
54
|
+
def call(*preambles)
|
55
|
+
output.append(preambles) unless preambles.empty?
|
56
|
+
|
57
|
+
root.Traverse(
|
58
|
+
0,
|
59
|
+
proc { |element, depth| render_leave(element, depth) },
|
60
|
+
) { |element, depth| render_enter(element, depth) }
|
61
|
+
|
62
|
+
output.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def append(depth, *lines)
|
66
|
+
unless lines.empty?
|
67
|
+
indentation = indent(depth)
|
68
|
+
output.append(lines.map { "#{indentation}#{_1}" })
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def build
|
75
|
+
ArgumentError.("Missing style") unless options[:style]
|
76
|
+
|
77
|
+
case options[:style]
|
78
|
+
when :hybrid then extend(Attributes::Hybrid)
|
79
|
+
when :inline then extend(Attributes::Inline)
|
80
|
+
when :block then extend(Attributes::Block)
|
81
|
+
else ArgumentError.("Unrecognized style: #{options[:style]}")
|
82
|
+
end
|
83
|
+
|
84
|
+
unclosed
|
85
|
+
end
|
86
|
+
|
87
|
+
def childless?(element)
|
88
|
+
element.children.empty? && element.contents.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
def closed = @closed = true
|
92
|
+
|
93
|
+
def closed? = @closed.tap { unclosed }
|
94
|
+
|
95
|
+
def attributes_block(element, depth, lines)
|
96
|
+
append(depth, "<#{element.name}")
|
97
|
+
append(depth + 1, *lines)
|
98
|
+
append(depth, ">") unless childless?(element)
|
99
|
+
end
|
100
|
+
|
101
|
+
def attributes_inline(element, depth, lines)
|
102
|
+
line = "<#{[element.name, *lines].join(" ")}"
|
103
|
+
|
104
|
+
append(depth, (childless?(element) ? "#{line}/>".tap { closed } : "#{line}>"))
|
105
|
+
end
|
106
|
+
|
107
|
+
def contents(element, depth)
|
108
|
+
return if element.contents.empty?
|
109
|
+
|
110
|
+
if floating?(element)
|
111
|
+
append(depth, *element.contents.map(&:to_s))
|
112
|
+
else
|
113
|
+
element.contents.each { |content| content.render(self, depth) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def indent(depth)
|
118
|
+
options[:indent] * depth
|
119
|
+
end
|
120
|
+
|
121
|
+
def render_enter(element, depth)
|
122
|
+
attributes(element, depth) unless floating?(element)
|
123
|
+
contents(element, depth)
|
124
|
+
end
|
125
|
+
|
126
|
+
def render_leave(element, depth)
|
127
|
+
return if closed? || floating?(element)
|
128
|
+
|
129
|
+
append(depth, (childless?(element) ? "/>" : "</#{element.name}>").to_s)
|
130
|
+
end
|
131
|
+
|
132
|
+
def unclosed = @closed = false
|
133
|
+
|
134
|
+
def floating?(element) = element.name == :"-"
|
135
|
+
|
136
|
+
class << self
|
137
|
+
def call(root, **) = new(root, **).call(*root.class.preambles)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private_constant :Renderer
|
142
|
+
|
143
|
+
module InstanceMethods
|
144
|
+
def Render(**) = Renderer.(self, **)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Replicate
|
7
|
+
module InstanceMethods
|
8
|
+
def Replicate(nx: Undefined, dx: Undefined, ix: nil, ny: Undefined, dy: Undefined, iy: nil, id: nil, &block)
|
9
|
+
Ensure.without_block(block, nx:, dx:, ny:, dy:)
|
10
|
+
|
11
|
+
With do |base|
|
12
|
+
layer(id:) do
|
13
|
+
row = layer do
|
14
|
+
(nx - 1).times do |time|
|
15
|
+
base.DuplicateH((time + 1) * dx, parent: self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
base.AdoptFirst(row)
|
20
|
+
|
21
|
+
(ny - 1).times do |time|
|
22
|
+
row.DuplicateV((time + 1) * dy)
|
23
|
+
end
|
24
|
+
end.tap do |element|
|
25
|
+
IdentifyAsTable(element.children, ix:, iy:)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def ReplicateH(n: Undefined, d: Undefined, i: nil, id: nil, &block)
|
31
|
+
Ensure.without_block(block, n:, d:)
|
32
|
+
|
33
|
+
With do |base|
|
34
|
+
layer(id:) do
|
35
|
+
(n - 1).times do |time|
|
36
|
+
base.DuplicateH((time + 1) * d, parent: self)
|
37
|
+
end
|
38
|
+
end.tap do |element|
|
39
|
+
base.AdoptFirst(element)
|
40
|
+
IdentifyAsList(element.children, i)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ReplicateV(n: Undefined, d: Undefined, i: nil, id: nil, &block)
|
46
|
+
Ensure.without_block(block, n:, d:)
|
47
|
+
|
48
|
+
With do |base|
|
49
|
+
layer(id:) do
|
50
|
+
(n - 1).times do |time|
|
51
|
+
base.DuplicateV((time + 1) * d, parent: self)
|
52
|
+
end
|
53
|
+
end.tap do |element|
|
54
|
+
base.AdoptFirst(element)
|
55
|
+
IdentifyAsList(element.children, i)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def Tile(symbol = Undefined, nx: Undefined, dx: Undefined, ix: nil, ny: Undefined, dy: Undefined, iy: nil, id: nil, &block)
|
61
|
+
Ensure.with_block(block, symbol:, nx:, dx:, ny:, dy:)
|
62
|
+
|
63
|
+
symbol(id: symbol).Within(&block)
|
64
|
+
use("xlink:href": "##{symbol}").Replicate(nx:, dx:, ix:, ny:, dy:, iy:, id:)
|
65
|
+
end
|
66
|
+
|
67
|
+
def TileH(symbol = Undefined, n: Undefined, d: Undefined, i: nil, id: nil, &block)
|
68
|
+
Ensure.with_block(block, symbol:, n:, d:)
|
69
|
+
|
70
|
+
symbol(id: symbol).Within(&block)
|
71
|
+
use("xlink:href": "##{symbol}").ReplicateH(n:, d:, i:, id:)
|
72
|
+
end
|
73
|
+
|
74
|
+
def TileV(symbol = Undefined, n: Undefined, d: Undefined, i: nil, id: nil, &block)
|
75
|
+
Ensure.with_block(block, symbol:, n:, d:)
|
76
|
+
|
77
|
+
symbol(id: symbol).Within(&block)
|
78
|
+
use("xlink:href": "##{symbol}").ReplicateV(n:, d:, i:, id:)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module Ensure
|
83
|
+
extend self
|
84
|
+
|
85
|
+
ASSERTION = {
|
86
|
+
symbol: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
|
87
|
+
n: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
|
88
|
+
nx: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
|
89
|
+
ny: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
|
90
|
+
d: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
91
|
+
dx: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
92
|
+
dy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
|
93
|
+
}.freeze
|
94
|
+
|
95
|
+
def with_block(block, **)
|
96
|
+
ArgumentError.("Block required") unless block
|
97
|
+
|
98
|
+
call(**)
|
99
|
+
end
|
100
|
+
|
101
|
+
def without_block(block, **)
|
102
|
+
ArgumentError.("Block not allowed") if block
|
103
|
+
|
104
|
+
call(**)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def call(**kwargs)
|
110
|
+
kwargs.each do |name, value|
|
111
|
+
issue = "Argument '#{name}' required" if value == Undefined
|
112
|
+
|
113
|
+
unless issue
|
114
|
+
next unless (assertion = ASSERTION[name])
|
115
|
+
next unless (issue = assertion.call(name, value))
|
116
|
+
end
|
117
|
+
|
118
|
+
ArgumentError.(issue)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Save
|
7
|
+
module InstanceMethods
|
8
|
+
EXT = ".svg"
|
9
|
+
|
10
|
+
def Out(*, **) = F.out(self.(**), *)
|
11
|
+
|
12
|
+
def Out!(*, **) = F.out(self.(**), *, smart: true)
|
13
|
+
|
14
|
+
def Save(dir = ".", **) = Out(Savefile(caller_locations(1..1).first.path, dir), **)
|
15
|
+
|
16
|
+
def Save!(dir = ".", **) = Out!(Savefile(caller_locations(1..1).first.path, dir), **)
|
17
|
+
|
18
|
+
def Savefile(path, dir = ".") = ::File.expand_path(::File.join(dir, "#{::File.basename(path, ".*")}#{EXT}"), ::File.dirname(path))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|