dieses 0.0.1
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/BEN/304/260OKU.md +0 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.md +675 -0
- data/README.md +18 -0
- data/bin/dieses +10 -0
- data/bin/diesis +10 -0
- data/dieses.gemspec +36 -0
- data/lib/dieses.rb +7 -0
- data/lib/dieses/application.rb +24 -0
- data/lib/dieses/application/batch.rb +127 -0
- data/lib/dieses/application/canvas.rb +51 -0
- data/lib/dieses/application/cli.rb +6 -0
- data/lib/dieses/application/cli/multi.rb +106 -0
- data/lib/dieses/application/cli/single.rb +100 -0
- data/lib/dieses/application/common.rb +27 -0
- data/lib/dieses/application/mixins.rb +5 -0
- data/lib/dieses/application/mixins/lines.rb +105 -0
- data/lib/dieses/application/mixins/scribes.rb +91 -0
- data/lib/dieses/application/mixins/squares.rb +23 -0
- data/lib/dieses/application/paper.rb +146 -0
- data/lib/dieses/application/pen.rb +161 -0
- data/lib/dieses/application/sheet.rb +111 -0
- data/lib/dieses/application/sheets.rb +58 -0
- data/lib/dieses/application/sheets/copperplate.rb +20 -0
- data/lib/dieses/application/sheets/cursive.rb +19 -0
- data/lib/dieses/application/sheets/graph.rb +27 -0
- data/lib/dieses/application/sheets/italics.rb +19 -0
- data/lib/dieses/application/sheets/lettering.rb +30 -0
- data/lib/dieses/application/sheets/lined.rb +24 -0
- data/lib/dieses/application/sheets/print.rb +19 -0
- data/lib/dieses/application/sheets/ruled.rb +25 -0
- data/lib/dieses/application/sheets/spencerian.rb +20 -0
- data/lib/dieses/application/sheets/thumbnail.rb +37 -0
- data/lib/dieses/error.rb +5 -0
- data/lib/dieses/geometry.rb +8 -0
- data/lib/dieses/geometry/element.rb +66 -0
- data/lib/dieses/geometry/equation.rb +57 -0
- data/lib/dieses/geometry/equation/slant.rb +88 -0
- data/lib/dieses/geometry/equation/steep.rb +70 -0
- data/lib/dieses/geometry/error.rb +7 -0
- data/lib/dieses/geometry/line.rb +67 -0
- data/lib/dieses/geometry/point.rb +98 -0
- data/lib/dieses/geometry/rect.rb +173 -0
- data/lib/dieses/geometry/support.rb +3 -0
- data/lib/dieses/support.rb +9 -0
- data/lib/dieses/support/class.rb +100 -0
- data/lib/dieses/support/const.rb +78 -0
- data/lib/dieses/support/enum.rb +63 -0
- data/lib/dieses/support/float.rb +43 -0
- data/lib/dieses/support/hash.rb +19 -0
- data/lib/dieses/support/kernel.rb +36 -0
- data/lib/dieses/support/math.rb +20 -0
- data/lib/dieses/version.rb +5 -0
- metadata +226 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dieses
|
4
|
+
module Geometry
|
5
|
+
class Element
|
6
|
+
BoundingBox = Struct.new :minimum, :maximum
|
7
|
+
|
8
|
+
attr_reader :attributes, :hash
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@attributes = {}
|
12
|
+
@hash = self.class.hash ^ to_h.hash
|
13
|
+
freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
def attr(**kwargs)
|
17
|
+
tap do
|
18
|
+
kwargs.each { |key, value| attributes[key.to_sym] = value }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def classify(*tags, **kwargs)
|
23
|
+
existing_class = attributes[:class] || Set.new
|
24
|
+
attr(**kwargs, tags: existing_class.add(tags))
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_svg
|
28
|
+
format to_svgf, attributes: Support.kwargs_to_s(**attributes)
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(other)
|
32
|
+
return false unless other.is_a? self.class
|
33
|
+
|
34
|
+
to_h == other.to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
alias == eql?
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def centered(elements, rect)
|
42
|
+
bbox = bounding_box_of(*elements)
|
43
|
+
|
44
|
+
x = (rect.width - bbox.maximum.x + bbox.minimum.x) / 2
|
45
|
+
y = (rect.height - bbox.maximum.y + bbox.minimum.y) / 2
|
46
|
+
|
47
|
+
elements.map { |element| element.translate(x: x, y: y).attr(**element.attributes.dup) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_svg(elements, rect = Undefined, prefix: EMPTY_STRING)
|
51
|
+
(Undefined.equal?(rect) ? elements : centered(elements, rect)).map do |element|
|
52
|
+
"#{prefix}#{element.to_svg}"
|
53
|
+
end.join.chomp
|
54
|
+
end
|
55
|
+
|
56
|
+
def bounding_box_of(*elements)
|
57
|
+
bboxes = elements.map(&:bbox)
|
58
|
+
|
59
|
+
minimum = bboxes.map(&:minimum).min
|
60
|
+
maximum = bboxes.map(&:maximum).max
|
61
|
+
|
62
|
+
Element::BoundingBox.new minimum, maximum
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'equation/slant'
|
4
|
+
require_relative 'equation/steep'
|
5
|
+
|
6
|
+
module Dieses
|
7
|
+
module Geometry
|
8
|
+
module Equation
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def from_line(line)
|
12
|
+
starting, ending = line.starting, line.ending
|
13
|
+
|
14
|
+
if (c = starting.x) == ending.x
|
15
|
+
vertical(c)
|
16
|
+
elsif (c = starting.y) == ending.y
|
17
|
+
horizontal(c)
|
18
|
+
else
|
19
|
+
slope = (ending.y - starting.y) / (ending.x - starting.x)
|
20
|
+
intercept = starting.y - slope * starting.x
|
21
|
+
|
22
|
+
slant(slope: slope, intercept: intercept)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def slant(slope:, intercept:)
|
27
|
+
Slant.new slope: slope, intercept: intercept
|
28
|
+
end
|
29
|
+
|
30
|
+
def slant_from_direction(point:, angle:)
|
31
|
+
return horizontal(point.y) if (angle % 180).zero?
|
32
|
+
return vertical(point.x) if (angle % 90).zero?
|
33
|
+
|
34
|
+
slope = Math.tan(Support.to_radian(angle.to_f))
|
35
|
+
intercept = point.y - slope * point.x
|
36
|
+
|
37
|
+
slant(slope: slope, intercept: intercept)
|
38
|
+
end
|
39
|
+
|
40
|
+
def steep(c)
|
41
|
+
Steep.new c
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
alias vertical steep
|
46
|
+
end
|
47
|
+
|
48
|
+
def horizontal(c)
|
49
|
+
slant(slope: 0.0, intercept: c)
|
50
|
+
end
|
51
|
+
|
52
|
+
def intersect(u, v)
|
53
|
+
u.intersect(v)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dieses
|
4
|
+
module Geometry
|
5
|
+
module Equation
|
6
|
+
class Slant
|
7
|
+
attr_reader :slope, :intercept
|
8
|
+
|
9
|
+
def initialize(slope:, intercept:)
|
10
|
+
@slope, @intercept = slope.to_f, intercept.to_f
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def x(y)
|
15
|
+
(y - intercept) / slope
|
16
|
+
end
|
17
|
+
|
18
|
+
def y(x)
|
19
|
+
slope * x + intercept
|
20
|
+
end
|
21
|
+
|
22
|
+
# When distance given, y = m * x + n (m, n positive) equation moves to the right (x increases, y decreases)
|
23
|
+
def translate(distance = nil, x: nil, y: nil)
|
24
|
+
dx, dy = 0, 0
|
25
|
+
|
26
|
+
intercept = self.intercept
|
27
|
+
|
28
|
+
dx, dy = distance * Math.cos(angle_in_radian), -distance * Math.sin(angle_in_radian) if distance
|
29
|
+
|
30
|
+
dx += x if x
|
31
|
+
dy += y if y
|
32
|
+
|
33
|
+
intercept -= slope * dx
|
34
|
+
intercept += dy
|
35
|
+
|
36
|
+
self.class.new slope: slope, intercept: intercept
|
37
|
+
end
|
38
|
+
|
39
|
+
def angle_in_radian
|
40
|
+
Math.atan(slope)
|
41
|
+
end
|
42
|
+
|
43
|
+
def intersect(other)
|
44
|
+
case other
|
45
|
+
when Slant
|
46
|
+
x = (other.intercept - intercept) / (slope - other.slope)
|
47
|
+
y = slope * x + intercept
|
48
|
+
when Steep
|
49
|
+
x = other.x
|
50
|
+
y = y(x)
|
51
|
+
end
|
52
|
+
|
53
|
+
Point.new(x, y)
|
54
|
+
end
|
55
|
+
|
56
|
+
def left?(point, precision: Support::Float.precision)
|
57
|
+
Support.almost_greater_than(point.y, y(point.x), precision: precision)
|
58
|
+
end
|
59
|
+
|
60
|
+
def right?(point, precision: Support::Float.precision)
|
61
|
+
Support.almost_less_than(point.y, y(point.x), precision: precision)
|
62
|
+
end
|
63
|
+
|
64
|
+
def onto?(point, precision: Support::Float.precision)
|
65
|
+
Support.almost_equal(point.y, y(point.x), precision: precision)
|
66
|
+
end
|
67
|
+
|
68
|
+
def eql?(other)
|
69
|
+
self.class == other.class && to_h == other.to_h
|
70
|
+
end
|
71
|
+
|
72
|
+
alias == eql?
|
73
|
+
|
74
|
+
def hash
|
75
|
+
self.class.hash ^ to_h.hash
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_h
|
79
|
+
{ slope: scope, intercept: intercept }
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
"E(y = #{slope} * x + #{intercept})"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dieses
|
4
|
+
module Geometry
|
5
|
+
module Equation
|
6
|
+
class Steep
|
7
|
+
def initialize(c)
|
8
|
+
@x = c
|
9
|
+
end
|
10
|
+
|
11
|
+
def y(_ = nil)
|
12
|
+
Float::INFINITY
|
13
|
+
end
|
14
|
+
|
15
|
+
def x(_ = nil)
|
16
|
+
@x
|
17
|
+
end
|
18
|
+
|
19
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
20
|
+
def translate(distance = nil, x: nil, y: nil)
|
21
|
+
self.class.new self.x + (distance || 0.0) + (x || 0.0)
|
22
|
+
end
|
23
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
24
|
+
|
25
|
+
def intersect(other)
|
26
|
+
case other
|
27
|
+
when Slant
|
28
|
+
x = self.x
|
29
|
+
y = other.y(x)
|
30
|
+
when Steep
|
31
|
+
x = Float::INFINITY
|
32
|
+
y = Float::INFINITY
|
33
|
+
end
|
34
|
+
|
35
|
+
Point.new(x, y)
|
36
|
+
end
|
37
|
+
|
38
|
+
def left?(point, precision: Support::Float.precision)
|
39
|
+
Support.almost_less_than(point.x, x(point.y), precision: precision)
|
40
|
+
end
|
41
|
+
|
42
|
+
def right?(point, precision: Support::Float.precision)
|
43
|
+
Support.almost_greater_than(point.x, x(point.y), precision: precision)
|
44
|
+
end
|
45
|
+
|
46
|
+
def onto?(point, precision: Support::Float.precision)
|
47
|
+
Support.almost_equal(point.x, x(point.y), precision: precision)
|
48
|
+
end
|
49
|
+
|
50
|
+
def eql?(other)
|
51
|
+
self.class == other.class && x == other.x
|
52
|
+
end
|
53
|
+
|
54
|
+
alias == eql?
|
55
|
+
|
56
|
+
def hash
|
57
|
+
self.class.hash ^ to_h.hash
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_h
|
61
|
+
{ x: x }
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
"E(x = #{c})"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Dieses
|
6
|
+
module Geometry
|
7
|
+
class Line < Element
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@equation, :left?, :right?, :intersect
|
11
|
+
|
12
|
+
attr_reader :starting, :ending, :equation
|
13
|
+
|
14
|
+
def initialize(starting, ending)
|
15
|
+
@starting, @ending = Point.cast(starting), Point.cast(ending)
|
16
|
+
@equation = Equation.from_line(self)
|
17
|
+
|
18
|
+
super()
|
19
|
+
end
|
20
|
+
|
21
|
+
def translate(x: nil, y: nil)
|
22
|
+
starting, ending = [self.starting, self.ending].map { |point| point.translate(x: x, y: y) }
|
23
|
+
self.class.new starting, ending
|
24
|
+
end
|
25
|
+
|
26
|
+
def duplicate(x: nil, y: nil, count: 1)
|
27
|
+
lines, line = [], self
|
28
|
+
|
29
|
+
count.times do
|
30
|
+
lines << line
|
31
|
+
line = line.translate(x: x, y: y)
|
32
|
+
end
|
33
|
+
|
34
|
+
[lines, line]
|
35
|
+
end
|
36
|
+
|
37
|
+
def duplicates(x: nil, y: nil, count: 1)
|
38
|
+
lines, = duplicate(x: x, y: y, count: count)
|
39
|
+
lines
|
40
|
+
end
|
41
|
+
|
42
|
+
def onto?(point)
|
43
|
+
equation.onto?(point) && point >= starting && point <= ending
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_svgf
|
47
|
+
x1, y1, x2, y2 = [*starting.to_a, *ending.to_a].map { |value| Support.approx(value) }
|
48
|
+
|
49
|
+
<<~SVG
|
50
|
+
<line x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}" %{attributes}/>
|
51
|
+
SVG
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"L(#{starting}, #{ending})"
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_h
|
59
|
+
{ starting: starting, ending: ending }
|
60
|
+
end
|
61
|
+
|
62
|
+
def bbox
|
63
|
+
BoundingBox.new([starting, ending].min, [starting, ending].max)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dieses
|
4
|
+
module Geometry
|
5
|
+
class Point
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :x, :y, :hash
|
9
|
+
|
10
|
+
def initialize(x, y)
|
11
|
+
@x, @y = x.to_f, y.to_f
|
12
|
+
@hash = Point.hash ^ to_a.hash
|
13
|
+
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def translate(x: nil, y: nil)
|
18
|
+
self.class.new(self.x + (x || 0), self.y + (y || 0))
|
19
|
+
end
|
20
|
+
|
21
|
+
def distance(other = nil)
|
22
|
+
self.class.distance(self, other)
|
23
|
+
end
|
24
|
+
|
25
|
+
def approx(precision = nil)
|
26
|
+
self.class.new Support.approx(x, precision), Support.approx(y, precision)
|
27
|
+
end
|
28
|
+
|
29
|
+
def <=>(other)
|
30
|
+
return unless other.is_a? Point
|
31
|
+
|
32
|
+
to_a <=> other.to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
def eql?(other)
|
36
|
+
return false unless other.is_a? Point
|
37
|
+
|
38
|
+
to_a == other.to_a
|
39
|
+
end
|
40
|
+
|
41
|
+
alias == eql?
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
"P(#{x}, #{y})"
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_a
|
48
|
+
[x, y]
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_h
|
52
|
+
{ x: x, y: y }
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def call(*args)
|
57
|
+
new(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def origin
|
61
|
+
new 0.0, 0.0
|
62
|
+
end
|
63
|
+
|
64
|
+
def distance(starting, ending)
|
65
|
+
ending ||= origin
|
66
|
+
Math.sqrt((ending.x - starting.x)**2 + (starting.y - ending.y)**2)
|
67
|
+
end
|
68
|
+
|
69
|
+
def cast(point)
|
70
|
+
Point.new(point.x, point.y)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Mutable < Point
|
75
|
+
attr_writer :x, :y
|
76
|
+
|
77
|
+
def initialize(x, y) # rubocop:disable Lint/MissingSuper
|
78
|
+
@x, @y = x.to_f, y.to_f
|
79
|
+
end
|
80
|
+
|
81
|
+
def hash
|
82
|
+
(@hash ||= self.class.hash) ^ to_a.hash
|
83
|
+
end
|
84
|
+
|
85
|
+
def translate!(x: nil, y: nil)
|
86
|
+
tap do
|
87
|
+
self.x += (x || 0)
|
88
|
+
self.y += (y || 0)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.cast(immutable_point)
|
93
|
+
Mutable.new(immutable_point.x, immutable_point.y)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|