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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE +674 -0
  4. data/README.md +15 -0
  5. data/bin/sevgi +136 -0
  6. data/lib/sevgi/errors.rb +11 -0
  7. data/lib/sevgi/external.rb +8 -0
  8. data/lib/sevgi/function.rb +35 -0
  9. data/lib/sevgi/geometry/elements/rect.rb +133 -0
  10. data/lib/sevgi/geometry/elements/segment.rb +102 -0
  11. data/lib/sevgi/geometry/elements.rb +41 -0
  12. data/lib/sevgi/geometry/equation/line/diagonal.rb +90 -0
  13. data/lib/sevgi/geometry/equation/line/horizontal.rb +15 -0
  14. data/lib/sevgi/geometry/equation/line/vertical.rb +52 -0
  15. data/lib/sevgi/geometry/equation/line.rb +38 -0
  16. data/lib/sevgi/geometry/equation.rb +3 -0
  17. data/lib/sevgi/geometry/errors.rb +5 -0
  18. data/lib/sevgi/geometry/external.rb +15 -0
  19. data/lib/sevgi/geometry/operation/align.rb +40 -0
  20. data/lib/sevgi/geometry/operation/sweep.rb +52 -0
  21. data/lib/sevgi/geometry/operation.rb +28 -0
  22. data/lib/sevgi/geometry/point.rb +62 -0
  23. data/lib/sevgi/geometry.rb +10 -0
  24. data/lib/sevgi/graphics/attribute.rb +125 -0
  25. data/lib/sevgi/graphics/canvas.rb +51 -0
  26. data/lib/sevgi/graphics/content.rb +72 -0
  27. data/lib/sevgi/graphics/document/base.rb +51 -0
  28. data/lib/sevgi/graphics/document/default.rb +19 -0
  29. data/lib/sevgi/graphics/document/html.rb +11 -0
  30. data/lib/sevgi/graphics/document/inkscape.rb +17 -0
  31. data/lib/sevgi/graphics/document/minimal.rb +11 -0
  32. data/lib/sevgi/graphics/document.rb +53 -0
  33. data/lib/sevgi/graphics/element.rb +85 -0
  34. data/lib/sevgi/graphics/external.rb +15 -0
  35. data/lib/sevgi/graphics/mixtures/core.rb +81 -0
  36. data/lib/sevgi/graphics/mixtures/duplicate.rb +36 -0
  37. data/lib/sevgi/graphics/mixtures/hatch.rb +21 -0
  38. data/lib/sevgi/graphics/mixtures/identify.rb +93 -0
  39. data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
  40. data/lib/sevgi/graphics/mixtures/lint.rb +33 -0
  41. data/lib/sevgi/graphics/mixtures/render.rb +149 -0
  42. data/lib/sevgi/graphics/mixtures/replicate.rb +125 -0
  43. data/lib/sevgi/graphics/mixtures/save.rb +23 -0
  44. data/lib/sevgi/graphics/mixtures/transform.rb +78 -0
  45. data/lib/sevgi/graphics/mixtures/underscore.rb +21 -0
  46. data/lib/sevgi/graphics/mixtures/validate.rb +27 -0
  47. data/lib/sevgi/graphics/mixtures/wrappers.rb +63 -0
  48. data/lib/sevgi/graphics/mixtures.rb +16 -0
  49. data/lib/sevgi/graphics.rb +10 -0
  50. data/lib/sevgi/internal/constant.rb +5 -0
  51. data/lib/sevgi/internal/dim.rb +13 -0
  52. data/lib/sevgi/internal/function/file.rb +30 -0
  53. data/lib/sevgi/internal/function/float.rb +35 -0
  54. data/lib/sevgi/internal/function/formula.rb +71 -0
  55. data/lib/sevgi/internal/function/math.rb +43 -0
  56. data/lib/sevgi/internal/function/string.rb +13 -0
  57. data/lib/sevgi/internal/function.rb +7 -0
  58. data/lib/sevgi/internal/list.rb +27 -0
  59. data/lib/sevgi/internal/locate.rb +47 -0
  60. data/lib/sevgi/internal/margin.rb +23 -0
  61. data/lib/sevgi/internal/minitest/script.rb +50 -0
  62. data/lib/sevgi/internal/minitest/shell.rb +71 -0
  63. data/lib/sevgi/internal/minitest/suite.rb +35 -0
  64. data/lib/sevgi/internal/minitest.rb +5 -0
  65. data/lib/sevgi/internal/paper.rb +26 -0
  66. data/lib/sevgi/internal.rb +10 -0
  67. data/lib/sevgi/standard/conform.rb +55 -0
  68. data/lib/sevgi/standard/data/attribute.rb +496 -0
  69. data/lib/sevgi/standard/data/element.rb +269 -0
  70. data/lib/sevgi/standard/data/specification.rb +1841 -0
  71. data/lib/sevgi/standard/data.rb +79 -0
  72. data/lib/sevgi/standard/errors.rb +28 -0
  73. data/lib/sevgi/standard/model.rb +82 -0
  74. data/lib/sevgi/standard.rb +26 -0
  75. data/lib/sevgi/utensils/external.rb +22 -0
  76. data/lib/sevgi/utensils/grid.rb +49 -0
  77. data/lib/sevgi/utensils/ruler.rb +47 -0
  78. data/lib/sevgi/utensils/tsquare.rb +28 -0
  79. data/lib/sevgi/utensils.rb +7 -0
  80. data/lib/sevgi/version.rb +5 -0
  81. data/lib/sevgi.rb +10 -0
  82. 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