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
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Diesis
|
2
|
+
======
|
3
|
+
|
4
|
+
[](https://github.com/alaturka/dieses/actions/workflows/test.yml)
|
5
|
+
[](https://codebeat.co/projects/github-com-alaturka-dieses-master)
|
6
|
+
|
7
|
+
Guide sheets for penmanship, calligraphy, lettering, and sketching.
|
8
|
+
[Click](https://github.com/alaturka/dieses/tree/master/docs/sheets) to reach the all available guide sheet combinations.
|
9
|
+
|
10
|
+
Couldn't find the guide sheet you were looking for? [Open an
|
11
|
+
issue](https://github.com/alaturka/dieses/issues/new/choose) and let's see what we can do.
|
12
|
+
|
13
|
+
Install
|
14
|
+
-------
|
15
|
+
|
16
|
+
```sh
|
17
|
+
gem install dieses
|
18
|
+
```
|
data/bin/dieses
ADDED
data/bin/diesis
ADDED
data/dieses.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
|
5
|
+
require 'dieses/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'dieses'
|
9
|
+
s.author = 'Recai Oktaş'
|
10
|
+
s.email = 'roktas@gmail.com'
|
11
|
+
s.license = 'GPL-3.0-or-later'
|
12
|
+
s.version = Dieses::VERSION.dup
|
13
|
+
s.summary = 'Guide sheets generator for penmanship, calligraphy, lettering, and sketching'
|
14
|
+
s.description = 'Guide sheets generator for penmanship, calligraphy, lettering, and sketching'
|
15
|
+
|
16
|
+
s.homepage = 'https://alaturka.github.io/dieses'
|
17
|
+
s.files = Dir['CHANGELOG.md', 'LICENSE.md', 'README.md', 'BENİOKU.md', 'dieses.gemspec', 'lib/**/*']
|
18
|
+
s.executables = %w[diesis dieses]
|
19
|
+
s.require_paths = %w[lib]
|
20
|
+
|
21
|
+
s.metadata['changelog_uri'] = 'https://github.com/alaturka/dieses/blob/master/CHANGELOG.md'
|
22
|
+
s.metadata['source_code_uri'] = 'https://github.com/alaturka/dieses'
|
23
|
+
s.metadata['bug_tracker_uri'] = 'https://github.com/alaturka/dieses/issues'
|
24
|
+
|
25
|
+
s.required_ruby_version = '>= 2.7.0' # rubocop:disable Gemspec/RequiredRubyVersion
|
26
|
+
|
27
|
+
s.add_development_dependency 'bundler'
|
28
|
+
s.add_development_dependency 'minitest-focus', '>= 1.2.1'
|
29
|
+
s.add_development_dependency 'minitest-reporters', '>= 1.4.3'
|
30
|
+
s.add_development_dependency 'rake'
|
31
|
+
s.add_development_dependency 'rubocop'
|
32
|
+
s.add_development_dependency 'rubocop-minitest'
|
33
|
+
s.add_development_dependency 'rubocop-performance'
|
34
|
+
s.add_development_dependency 'rubocop-rake'
|
35
|
+
s.add_development_dependency 'rubygems-tasks'
|
36
|
+
end
|
data/lib/dieses.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'application/common'
|
4
|
+
require_relative 'application/paper'
|
5
|
+
require_relative 'application/canvas'
|
6
|
+
require_relative 'application/pen'
|
7
|
+
require_relative 'application/mixins'
|
8
|
+
require_relative 'application/sheet'
|
9
|
+
require_relative 'application/sheets'
|
10
|
+
require_relative 'application/batch'
|
11
|
+
|
12
|
+
module Dieses
|
13
|
+
module Application
|
14
|
+
module_function
|
15
|
+
|
16
|
+
def produce(sheet, variant: Undefined, paper: Paper.default, orientation: Orientation.default, **render_args)
|
17
|
+
sheet(sheet, variant: variant, paper: paper, orientation: orientation).produce(**render_args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sheet(sheet, variant: Undefined, paper: Paper.default, orientation: Orientation.default)
|
21
|
+
Sheets.sheet(sheet.to_sym).new(Paper.public_send(paper.to_sym).orient(orientation.to_sym), variant: variant)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Dieses
|
7
|
+
module Application
|
8
|
+
module Batch
|
9
|
+
Production = Struct.new :sheet, :variant, :desc, :paper, :orientation, :file, :produce, keyword_init: true do
|
10
|
+
def self.call(**kwargs)
|
11
|
+
new(**kwargs.slice(*members)).tap do |production|
|
12
|
+
production.file = production.relfile unless production.file
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
suffix = orientation.to_s == 'landscape' ? 'l' : ''
|
18
|
+
"#{sheet}-#{variant}@#{paper}#{suffix}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def key
|
22
|
+
to_s.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def relfile
|
26
|
+
File.join(reldir, "#{self}.svg")
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
Application.produce(sheet, variant: variant, paper: paper, orientation: orientation)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run(basedir)
|
34
|
+
unless produce
|
35
|
+
warn "Undefined production request; skipping: #{self}" if produce.nil?
|
36
|
+
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
outfile = File.join(File.expand_path(basedir), file)
|
41
|
+
content = call
|
42
|
+
|
43
|
+
FileUtils.mkdir_p(File.dirname(outfile))
|
44
|
+
File.write(outfile, content)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def reldir
|
50
|
+
File.join(sheet, paper)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private_constant :Production
|
55
|
+
|
56
|
+
module_function
|
57
|
+
|
58
|
+
PAPERS = %i[a4 a5].freeze
|
59
|
+
ORIENTATIONS = Orientation.values
|
60
|
+
|
61
|
+
# rubocop:disable Metrics/MethodLength
|
62
|
+
# codebeat:disable[BLOCK_NESTING]
|
63
|
+
def all(papers: PAPERS, orientations: ORIENTATIONS)
|
64
|
+
Set.new.tap do |productions|
|
65
|
+
papers.each do |paper|
|
66
|
+
Sheets.available.each do |sheet, proto|
|
67
|
+
proto.variants.each do |variant|
|
68
|
+
orientations.each do |orientation|
|
69
|
+
productions << Production.(sheet: sheet.to_s,
|
70
|
+
variant: variant.to_s,
|
71
|
+
desc: variant.desc,
|
72
|
+
paper: paper.to_s,
|
73
|
+
orientation: orientation.to_s)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# codebeat:enable[BLOCK_NESTING]
|
81
|
+
|
82
|
+
def defaults
|
83
|
+
Set.new(
|
84
|
+
Sheets.defaults.map do |sheet, variant|
|
85
|
+
Production.(sheet: sheet.to_s,
|
86
|
+
variant: variant.to_s,
|
87
|
+
desc: variant.desc,
|
88
|
+
paper: Paper.default.to_s,
|
89
|
+
orientation: Orientation.default.to_s)
|
90
|
+
end
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def index(productions)
|
95
|
+
previous = Support.hashify_by productions, :key
|
96
|
+
current = Support.hashify_by Batch.all, :key
|
97
|
+
|
98
|
+
unprocessed = []
|
99
|
+
current.each do |name, production|
|
100
|
+
unless previous.key?(name)
|
101
|
+
unprocessed << production
|
102
|
+
|
103
|
+
next
|
104
|
+
end
|
105
|
+
|
106
|
+
production.produce = previous[name].produce
|
107
|
+
end
|
108
|
+
|
109
|
+
[current.values, unprocessed]
|
110
|
+
end
|
111
|
+
# rubocop:enable Metrics/MethodLength
|
112
|
+
|
113
|
+
def from_json_file(file)
|
114
|
+
JSON.load_file(file).map do |hash|
|
115
|
+
hash.transform_keys!(&:to_sym)
|
116
|
+
Production.(**hash.slice(*Production.members))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_json_file(file, productions)
|
121
|
+
content = JSON.pretty_generate(productions.map(&:to_h)).chomp
|
122
|
+
|
123
|
+
File.write(file, "#{content}\n")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Dieses
|
6
|
+
module Application
|
7
|
+
class Canvas < DelegateClass(Geometry::Rect)
|
8
|
+
TEMPLATE = <<~XML
|
9
|
+
<?xml version="1.0" standalone="no"?>
|
10
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
11
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="%{width}mm" height="%{height}mm" viewBox="0 0 %{width} %{height}" %{header}>
|
12
|
+
%{style}
|
13
|
+
<g id="sheet">
|
14
|
+
%{content}
|
15
|
+
</g>
|
16
|
+
</svg>
|
17
|
+
XML
|
18
|
+
|
19
|
+
attr_reader :elements, :paper
|
20
|
+
|
21
|
+
def initialize(paper = Paper.a4)
|
22
|
+
super(paper.inner)
|
23
|
+
|
24
|
+
@paper = paper
|
25
|
+
@elements = Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
@paper.to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<(items)
|
33
|
+
[*items].each do |item|
|
34
|
+
case item
|
35
|
+
when Array then item.each { |element| elements << element }
|
36
|
+
when Geometry::Element then elements << item
|
37
|
+
else raise Error, 'Item must be an Array or Element'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def render(header: EMPTY_STRING, style: EMPTY_STRING)
|
43
|
+
# We avoid prettifying XML through REXML which is pretty slow, at the cost of some ugly hacks.
|
44
|
+
format(TEMPLATE, **to_h,
|
45
|
+
content: Geometry.to_svg(elements, paper, prefix: ' ' * 4),
|
46
|
+
header: header,
|
47
|
+
style: style.empty? ? '' : format('<style>%{style}</style>', style: style))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module Dieses
|
7
|
+
module Application
|
8
|
+
module CLI
|
9
|
+
module Multi
|
10
|
+
OPTIONS = EMPTY_HASH
|
11
|
+
|
12
|
+
def self.call(*argv, **options)
|
13
|
+
options = OpenStruct.new(OPTIONS.merge(options))
|
14
|
+
args options(argv, options), argv
|
15
|
+
|
16
|
+
index_file = argv.first
|
17
|
+
|
18
|
+
return build_index(index_file, options) if options.index
|
19
|
+
|
20
|
+
batch_run(index_file, options)
|
21
|
+
rescue OptionParser::InvalidOption, Dieses::Error => e
|
22
|
+
abort(e.message)
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
private
|
27
|
+
|
28
|
+
def options(argv, options) # rubocop:disable Metrics/MethodLength
|
29
|
+
Signal.trap('INT') { Kernel.abort '' }
|
30
|
+
|
31
|
+
OptionParser.new do |option|
|
32
|
+
program_name = option.program_name
|
33
|
+
option.banner = <<~BANNER
|
34
|
+
Usage: #{program_name} [options...] <INDEXFILE>
|
35
|
+
|
36
|
+
See #{program_name}(1) manual page for detailed help.
|
37
|
+
|
38
|
+
Options:
|
39
|
+
|
40
|
+
BANNER
|
41
|
+
|
42
|
+
option.on('--index', 'Create index for all variants without producing sheets') do |opt|
|
43
|
+
options.index = opt
|
44
|
+
end
|
45
|
+
|
46
|
+
option.on('--destdir DIR', 'Destination directory') do |opt|
|
47
|
+
options.destdir = opt
|
48
|
+
end
|
49
|
+
|
50
|
+
option.on('--no-clobber', 'Do not overwrite an existing file or directory') do
|
51
|
+
options.no_clobber = true
|
52
|
+
end
|
53
|
+
|
54
|
+
option.on('--pdf', 'Generate PDF') do |_opt|
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
option.on_tail('-h', '--help', 'Show this message') do
|
59
|
+
abort option.help
|
60
|
+
end
|
61
|
+
|
62
|
+
option.on_tail('-v', '--version', 'Show version') do
|
63
|
+
warn VERSION
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
end.tap { |parser| parser.parse!(argv) } # rubocop:disable Style/MultilineBlockChain
|
67
|
+
end
|
68
|
+
|
69
|
+
def args(parser, argv)
|
70
|
+
if argv.empty?
|
71
|
+
warn parser.help
|
72
|
+
warn ''
|
73
|
+
abort 'Error: Index file required.'
|
74
|
+
end
|
75
|
+
|
76
|
+
return if argv.size <= 1
|
77
|
+
|
78
|
+
warn parser.help
|
79
|
+
warn ''
|
80
|
+
abort 'Error: Too many arguments.'
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_index(index_file, options)
|
84
|
+
abort("Index file exists: #{index_file}") if options.no_clobber && File.exist?(index_file)
|
85
|
+
|
86
|
+
productions, unprocessed = Batch.index Batch.from_json_file(index_file)
|
87
|
+
|
88
|
+
warn "Warning: There are #{unprocessed.size} new sheets which should be processed" unless unprocessed.empty?
|
89
|
+
|
90
|
+
Batch.to_json_file(index_file, productions)
|
91
|
+
end
|
92
|
+
|
93
|
+
def batch_run(index_file, options)
|
94
|
+
abort("Index file not found: #{index_file}") unless File.exist?(index_file)
|
95
|
+
|
96
|
+
destdir = options.destdir || File.join(File.dirname(index_file))
|
97
|
+
abort("Destination directory exists: #{destdir}") if Dir.exist?(destdir) && options.no_clobber
|
98
|
+
|
99
|
+
FileUtils.rm_rf(Dir[File.join(destdir, '*')].select { |file| File.directory?(file) })
|
100
|
+
Batch.from_json_file(index_file).each { |production| production.run(destdir) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dieses
|
4
|
+
module Application
|
5
|
+
module CLI
|
6
|
+
module Single
|
7
|
+
def self.call(*argv, **options)
|
8
|
+
options = OpenStruct.new(options)
|
9
|
+
args options(argv, options), argv
|
10
|
+
|
11
|
+
sheet = argv.first
|
12
|
+
|
13
|
+
return puts(out = Application.produce(sheet, **options.to_h)) unless options[:output]
|
14
|
+
|
15
|
+
File.write(options[:output], out)
|
16
|
+
rescue OptionParser::InvalidOption, Dieses::Error => e
|
17
|
+
abort(e.message)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
private
|
22
|
+
|
23
|
+
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
24
|
+
# codebeat:disable[LOC]
|
25
|
+
def options(argv, options)
|
26
|
+
Signal.trap('INT') { Kernel.abort '' }
|
27
|
+
|
28
|
+
OptionParser.new do |option| # rubocop:disable Metrics/BlockLength
|
29
|
+
program_name = option.program_name
|
30
|
+
option.banner = <<~BANNER
|
31
|
+
Usage: #{program_name} [options...] <SHEET>
|
32
|
+
|
33
|
+
See #{program_name}(1) manual page for detailed help.
|
34
|
+
|
35
|
+
Options:
|
36
|
+
|
37
|
+
BANNER
|
38
|
+
|
39
|
+
option.on('--paper PAPER', 'Paper size [default: a4]', String) do |opt|
|
40
|
+
options.paper = opt
|
41
|
+
end
|
42
|
+
|
43
|
+
option.on('--variant VARIANT', 'Sheet variant', String) do |opt|
|
44
|
+
options.variant = opt
|
45
|
+
end
|
46
|
+
|
47
|
+
option.on('--landscape', 'Use landscape orientation') do
|
48
|
+
options.orientation = :landscape
|
49
|
+
end
|
50
|
+
|
51
|
+
option.on('-o', '--output FILE', 'Output file', String) do |opt|
|
52
|
+
options.output = opt
|
53
|
+
end
|
54
|
+
|
55
|
+
option.on('--pdf', 'Generate PDF') do |_opt|
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
option.on('-l', '--list', 'List guidesheets') do
|
60
|
+
list
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
|
64
|
+
option.on_tail('-h', '--help', 'Show this message') do
|
65
|
+
abort option.help
|
66
|
+
end
|
67
|
+
|
68
|
+
option.on_tail('-v', '--version', 'Show version') do
|
69
|
+
warn VERSION
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
end.tap { |parser| parser.parse!(argv) } # rubocop:disable Style/MultilineBlockChain
|
73
|
+
end
|
74
|
+
# codebeat:enable[LOC]
|
75
|
+
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize
|
76
|
+
|
77
|
+
def args(parser, argv)
|
78
|
+
if argv.empty?
|
79
|
+
warn parser.help
|
80
|
+
warn ''
|
81
|
+
abort "Error: Guidesheet type required. Type #{parser.program_name} -l to list all available guidesheets."
|
82
|
+
end
|
83
|
+
|
84
|
+
return if argv.size <= 1
|
85
|
+
|
86
|
+
warn parser.help
|
87
|
+
warn ''
|
88
|
+
abort 'Error: Too many arguments.'
|
89
|
+
end
|
90
|
+
|
91
|
+
def list
|
92
|
+
warn 'Sheets:'
|
93
|
+
warn ''
|
94
|
+
warn Sheets.dump(prefix: "\t")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|