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,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
|