squib 0.13.4 → 0.14.beta1
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 +4 -4
- data/.travis.yml +2 -4
- data/CHANGELOG.md +19 -3
- data/README.md +1 -1
- data/bin/squib +1 -16
- data/lib/squib.rb +1 -1
- data/lib/squib/api/data.rb +29 -0
- data/lib/squib/api/save.rb +23 -2
- data/lib/squib/api/shapes.rb +37 -0
- data/lib/squib/api/text.rb +1 -1
- data/lib/squib/api/units.rb +5 -0
- data/lib/squib/args/arg_loader.rb +1 -1
- data/lib/squib/args/coords.rb +3 -1
- data/lib/squib/args/dir_validator.rb +1 -1
- data/lib/squib/args/sheet.rb +9 -2
- data/lib/squib/args/sprue_file.rb +44 -0
- data/lib/squib/args/unit_conversion.rb +2 -0
- data/lib/squib/{layouts → builtin/layouts}/economy.yml +5 -5
- data/lib/squib/{layouts → builtin/layouts}/fantasy.yml +4 -4
- data/lib/squib/{layouts → builtin/layouts}/hand.yml +0 -0
- data/lib/squib/builtin/layouts/party.yml +94 -0
- data/lib/squib/{layouts → builtin/layouts}/playing-card.yml +0 -0
- data/lib/squib/{layouts → builtin/layouts}/tuck_box.yml +1 -1
- data/lib/squib/{project_template → builtin/projects/advanced}/.gitignore +0 -0
- data/lib/squib/{project_template → builtin/projects/advanced}/ABOUT.md +0 -0
- data/lib/squib/builtin/projects/advanced/Gemfile +11 -0
- data/lib/squib/builtin/projects/advanced/Guardfile +21 -0
- data/lib/squib/{project_template → builtin/projects/advanced}/IDEAS.md +0 -0
- data/lib/squib/{project_template → builtin/projects/advanced}/PLAYTESTING.md +0 -0
- data/lib/squib/builtin/projects/advanced/Rakefile +27 -0
- data/lib/squib/{project_template → builtin/projects/advanced}/_output/gitkeep.txt +0 -0
- data/lib/squib/builtin/projects/advanced/config.yml +49 -0
- data/lib/squib/builtin/projects/advanced/data/game.xlsx +0 -0
- data/lib/squib/project_template/PNP NOTES.md b/data/lib/squib/builtin/projects/advanced/docs/PNP → NOTES.md +0 -0
- data/lib/squib/{project_template → builtin/projects/advanced/docs}/RULES.md +0 -0
- data/lib/squib/builtin/projects/advanced/img/example.svg +60 -0
- data/lib/squib/builtin/projects/advanced/layouts/deck.yml +27 -0
- data/lib/squib/builtin/projects/advanced/src/deck.rb +34 -0
- data/lib/squib/builtin/projects/advanced/src/version.rb +3 -0
- data/lib/squib/builtin/projects/basic/.gitignore +4 -0
- data/lib/squib/builtin/projects/basic/ABOUT.md +19 -0
- data/lib/squib/{project_template → builtin/projects/basic}/Gemfile +0 -0
- data/lib/squib/builtin/projects/basic/IDEAS.md +22 -0
- data/lib/squib/builtin/projects/basic/PLAYTESTING.md +26 -0
- data/lib/squib/builtin/projects/basic/PNP NOTES.md +4 -0
- data/lib/squib/builtin/projects/basic/RULES.md +21 -0
- data/lib/squib/{project_template → builtin/projects/basic}/Rakefile +0 -0
- data/lib/squib/builtin/projects/basic/_output/gitkeep.txt +1 -0
- data/lib/squib/{project_template → builtin/projects/basic}/config.yml +0 -0
- data/lib/squib/{project_template → builtin/projects/basic}/deck.rb +0 -0
- data/lib/squib/{project_template → builtin/projects/basic}/layout.yml +0 -0
- data/lib/squib/builtin/sprues/a4_euro_card.yml +42 -0
- data/lib/squib/builtin/sprues/a4_poker_card_8up.yml +40 -0
- data/lib/squib/builtin/sprues/a4_poker_card_9up.yml +42 -0
- data/lib/squib/builtin/sprues/a4_usa_card.yml +42 -0
- data/lib/squib/builtin/sprues/letter_poker_card_9up.yml +25 -0
- data/lib/squib/builtin/sprues/letter_poker_foldable_8up.yml +52 -0
- data/lib/squib/card.rb +1 -1
- data/lib/squib/commands/cli.rb +39 -0
- data/lib/squib/commands/data/template_option.rb +109 -0
- data/lib/squib/commands/make_sprue.rb +275 -0
- data/lib/squib/commands/new.rb +37 -4
- data/lib/squib/constants.rb +6 -1
- data/lib/squib/graphics/cairo_context_wrapper.rb +1 -1
- data/lib/squib/graphics/image.rb +6 -1
- data/lib/squib/graphics/save_doc.rb +6 -4
- data/lib/squib/graphics/save_pdf.rb +13 -8
- data/lib/squib/graphics/save_sprue.rb +228 -0
- data/lib/squib/graphics/shapes.rb +8 -2
- data/lib/squib/graphics/text.rb +4 -3
- data/lib/squib/layout_parser.rb +17 -1
- data/lib/squib/sample_helpers.rb +1 -1
- data/lib/squib/sprues/crop_line.rb +28 -0
- data/lib/squib/sprues/crop_line_dash.rb +35 -0
- data/lib/squib/sprues/invalid_sprue_definition.rb +9 -0
- data/lib/squib/sprues/sprue.rb +203 -0
- data/lib/squib/sprues/sprue_schema.rb +48 -0
- data/lib/squib/version.rb +1 -1
- data/samples/autoscale_font/_autoscale_font.rb +3 -3
- data/samples/backend/_backend.rb +1 -1
- data/samples/basic.rb +2 -2
- data/samples/colors/_colors.rb +1 -1
- data/samples/colors/_gradients.rb +1 -1
- data/samples/config/config_text_markup.rb +3 -3
- data/samples/config/custom_config.rb +1 -1
- data/samples/data/_csv.rb +3 -3
- data/samples/data/_excel.rb +4 -4
- data/samples/data/_yaml.rb +12 -0
- data/samples/images/_images.rb +2 -2
- data/samples/images/_more_load_images.rb +3 -0
- data/samples/images/_unicode.rb +2 -2
- data/samples/intro/02_options.rb +2 -2
- data/samples/layouts/builtin_layouts.rb +27 -4
- data/samples/layouts/layouts.rb +2 -2
- data/samples/proofs/_tgc_proofs.rb +2 -2
- data/samples/ranges/_ranges.rb +3 -3
- data/samples/saves/_hand.rb +1 -1
- data/samples/saves/_save_pdf.rb +4 -0
- data/samples/saves/_saves.rb +7 -1
- data/samples/saves/_showcase.rb +1 -1
- data/samples/shapes/_draw_shapes.rb +5 -1
- data/samples/shapes/_proofs.rb +22 -0
- data/samples/sprues/_builtin_sprues.rb +19 -0
- data/samples/sprues/_fold_sheet.rb +27 -0
- data/samples/sprues/_hex_tiles.rb +15 -0
- data/samples/sprues/_mints.rb +11 -0
- data/samples/sprues/_sprue_example.rb +11 -0
- data/samples/text/_embed_text.rb +14 -14
- data/samples/text/_text.rb +8 -8
- data/samples/text/_text_options.rb +17 -17
- data/squib.gemspec +18 -17
- metadata +126 -44
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
module Squib
|
|
2
|
+
module Graphics
|
|
3
|
+
# Helper class to generate templated sheet.
|
|
4
|
+
class SaveSprue
|
|
5
|
+
def initialize(deck, tmpl, outfile)
|
|
6
|
+
@deck = deck
|
|
7
|
+
@tmpl = tmpl
|
|
8
|
+
@page_number = 1
|
|
9
|
+
@outfile = outfile
|
|
10
|
+
@rotated_delta = (@tmpl.card_width - @deck.width).abs / 2
|
|
11
|
+
@overlay_lines = @tmpl.crop_lines.select do |line|
|
|
12
|
+
line['overlay_on_cards']
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_sheet(range)
|
|
17
|
+
cc = init_cc
|
|
18
|
+
cc.set_source_color(:white) # white backdrop TODO make option
|
|
19
|
+
cc.paint
|
|
20
|
+
card_set = @tmpl.cards
|
|
21
|
+
per_sheet = card_set.size
|
|
22
|
+
default_angle = @tmpl.card_default_rotation
|
|
23
|
+
if default_angle.zero?
|
|
24
|
+
default_angle = check_card_orientation
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
draw_overlay_below_cards cc if range.size
|
|
28
|
+
|
|
29
|
+
track_progress(range) do |bar|
|
|
30
|
+
range.each do |i|
|
|
31
|
+
cc = next_page_if_needed(cc, i, per_sheet)
|
|
32
|
+
|
|
33
|
+
card = @deck.cards[i]
|
|
34
|
+
slot = card_set[i % per_sheet]
|
|
35
|
+
x = slot['x']
|
|
36
|
+
y = slot['y']
|
|
37
|
+
angle = slot['rotate'] != 0 ? slot['rotate'] : default_angle
|
|
38
|
+
|
|
39
|
+
if angle != 0
|
|
40
|
+
draw_rotated_card cc, card, x, y, angle
|
|
41
|
+
else
|
|
42
|
+
cc.set_source card.cairo_surface, x, y
|
|
43
|
+
end
|
|
44
|
+
cc.paint
|
|
45
|
+
|
|
46
|
+
bar.increment
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
draw_overlay_above_cards cc
|
|
50
|
+
cc = draw_page cc
|
|
51
|
+
cc.target.finish
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
# Initialize the Cairo Context
|
|
58
|
+
def init_cc
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def draw_page(cc)
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def full_filename
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def next_page_if_needed(cc, i, per_sheet)
|
|
73
|
+
return cc unless (i != 0) && (i % per_sheet).zero?
|
|
74
|
+
|
|
75
|
+
draw_overlay_above_cards cc
|
|
76
|
+
cc = draw_page cc
|
|
77
|
+
draw_overlay_below_cards cc
|
|
78
|
+
@page_number += 1
|
|
79
|
+
cc
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def track_progress(range)
|
|
83
|
+
msg = "Saving templated sheet to #{full_filename}"
|
|
84
|
+
@deck.progress_bar.start(msg, range.size) { |bar| yield(bar) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def draw_overlay_below_cards(cc)
|
|
88
|
+
if @tmpl.crop_line_overlay == :on_margin
|
|
89
|
+
add_margin_overlay_clip_mask cc
|
|
90
|
+
cc.clip
|
|
91
|
+
draw_crop_line cc, @tmpl.crop_lines
|
|
92
|
+
cc.reset_clip
|
|
93
|
+
elsif @tmpl.crop_line_overlay == :beneath_cards
|
|
94
|
+
draw_crop_line cc, @tmpl.crop_lines
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def draw_overlay_above_cards(cc)
|
|
99
|
+
if @tmpl.crop_line_overlay == :overlay_on_cards
|
|
100
|
+
draw_crop_line cc, @tmpl.crop_lines
|
|
101
|
+
else
|
|
102
|
+
draw_crop_line cc, @overlay_lines
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_margin_overlay_clip_mask(cc)
|
|
107
|
+
margin = @tmpl.margin
|
|
108
|
+
cc.new_path
|
|
109
|
+
cc.rectangle(
|
|
110
|
+
margin[:left], margin[:top],
|
|
111
|
+
margin[:right] - margin[:left],
|
|
112
|
+
margin[:bottom] - margin[:top]
|
|
113
|
+
)
|
|
114
|
+
cc.new_sub_path
|
|
115
|
+
cc.move_to @tmpl.sheet_width, 0
|
|
116
|
+
cc.line_to 0, 0
|
|
117
|
+
cc.line_to 0, @tmpl.sheet_height
|
|
118
|
+
cc.line_to @tmpl.sheet_width, @tmpl.sheet_height
|
|
119
|
+
cc.close_path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def draw_crop_line(cc, crop_lines)
|
|
123
|
+
crop_lines.each do |line|
|
|
124
|
+
cc.move_to line['line'].x1, line['line'].y1
|
|
125
|
+
cc.line_to line['line'].x2, line['line'].y2
|
|
126
|
+
cc.set_source_color line['color']
|
|
127
|
+
cc.set_line_width line['width']
|
|
128
|
+
cc.set_dash(line['style'].pattern) if line['style'].pattern
|
|
129
|
+
cc.stroke
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def check_card_orientation
|
|
134
|
+
clockwise = 1.5 * Math::PI
|
|
135
|
+
if @deck.width <= @tmpl.card_width &&
|
|
136
|
+
@deck.height <= @tmpl.card_height
|
|
137
|
+
return 0
|
|
138
|
+
elsif (
|
|
139
|
+
@deck.width == @tmpl.card_height &&
|
|
140
|
+
@deck.height == @tmpl.card_width)
|
|
141
|
+
Squib.logger.warn {
|
|
142
|
+
'Rotating cards to match card orientation in template.'
|
|
143
|
+
}
|
|
144
|
+
return clockwise
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Squib.logger.warn {
|
|
148
|
+
'Card size is larger than sprue\'s expected card size. '\
|
|
149
|
+
'Cards may overlap.'
|
|
150
|
+
}
|
|
151
|
+
return 0
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def draw_rotated_card(cc, card, x, y, angle)
|
|
155
|
+
# Normalize the angles first
|
|
156
|
+
angle = angle % (2 * Math::PI)
|
|
157
|
+
angle = 2 * Math::PI - angle if angle < 0
|
|
158
|
+
|
|
159
|
+
# Determine what's the delta we need to translate our cards
|
|
160
|
+
delta_shift = @deck.width < @deck.height ? 1 : -1
|
|
161
|
+
if angle.zero? || angle == Math::PI
|
|
162
|
+
delta = 0
|
|
163
|
+
elsif angle < Math::PI
|
|
164
|
+
delta = -delta_shift * @rotated_delta
|
|
165
|
+
else
|
|
166
|
+
delta = delta_shift * @rotated_delta
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Perform the actual rotation and drawing
|
|
170
|
+
mat = cc.matrix # Save the transformation matrix to revert later
|
|
171
|
+
cc.translate x, y
|
|
172
|
+
cc.translate @deck.width / 2, @deck.height / 2
|
|
173
|
+
cc.rotate angle
|
|
174
|
+
cc.translate(-@deck.width / 2 + delta, -@deck.height / 2 + delta)
|
|
175
|
+
cc.set_source card.cairo_surface, 0, 0
|
|
176
|
+
cc.matrix = mat
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Templated sheet renderer in PDF format.
|
|
181
|
+
class SaveSpruePDF < SaveSprue
|
|
182
|
+
def init_cc
|
|
183
|
+
ratio = 72.0 / @deck.dpi
|
|
184
|
+
|
|
185
|
+
surface = Cairo::PDFSurface.new(
|
|
186
|
+
full_filename,
|
|
187
|
+
@tmpl.sheet_width * ratio,
|
|
188
|
+
@tmpl.sheet_height * ratio
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
cc = Cairo::Context.new(surface)
|
|
192
|
+
cc.scale(72.0 / @deck.dpi, 72.0 / @deck.dpi) # make it like pixels
|
|
193
|
+
cc
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def draw_page(cc)
|
|
197
|
+
cc.show_page
|
|
198
|
+
cc.set_source_color(:white) # white backdrop TODO make option
|
|
199
|
+
cc.paint
|
|
200
|
+
cc
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def full_filename
|
|
204
|
+
@outfile.full_filename
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Templated sheet renderer in PNG format.
|
|
209
|
+
class SaveSpruePNG < SaveSprue
|
|
210
|
+
def init_cc
|
|
211
|
+
surface = Cairo::ImageSurface.new @tmpl.sheet_width, @tmpl.sheet_height
|
|
212
|
+
Cairo::Context.new(surface)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def draw_page(cc)
|
|
216
|
+
cc.target.write_to_png(full_filename)
|
|
217
|
+
init_cc
|
|
218
|
+
cc.set_source_color(:white) # white backdrop TODO make option
|
|
219
|
+
cc.paint
|
|
220
|
+
cc
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def full_filename
|
|
224
|
+
@outfile.full_filename @page_number
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -18,8 +18,14 @@ module Squib
|
|
|
18
18
|
def circle(box, draw)
|
|
19
19
|
x, y, r = box.x, box.y, box.radius
|
|
20
20
|
use_cairo do |cc|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
if box.arc_direction == :clockwise
|
|
22
|
+
cc.arc(x, y, r, box.arc_start, box.arc_end)
|
|
23
|
+
else
|
|
24
|
+
cc.arc_negative(x, y, r, box.arc_start, box.arc_end)
|
|
25
|
+
end
|
|
26
|
+
if box.arc_close
|
|
27
|
+
cc.close_path();
|
|
28
|
+
end
|
|
23
29
|
cc.fill_n_stroke(draw)
|
|
24
30
|
end
|
|
25
31
|
end
|
data/lib/squib/graphics/text.rb
CHANGED
|
@@ -123,7 +123,7 @@ module Squib
|
|
|
123
123
|
|
|
124
124
|
# :nodoc:
|
|
125
125
|
# @api private
|
|
126
|
-
def text(embed, para, box, trans, draw)
|
|
126
|
+
def text(embed, para, box, trans, draw, dpi)
|
|
127
127
|
Squib.logger.debug {"Rendering text with: \n#{para} \nat:\n #{box} \ndraw:\n #{draw} \ntransform: #{trans}"}
|
|
128
128
|
extents = nil
|
|
129
129
|
use_cairo do |cc|
|
|
@@ -136,10 +136,11 @@ module Squib
|
|
|
136
136
|
font_desc.size = para.font_size * Pango::SCALE unless para.font_size.nil?
|
|
137
137
|
layout = cc.create_pango_layout
|
|
138
138
|
layout.font_description = font_desc
|
|
139
|
-
layout.text = para.str
|
|
139
|
+
layout.text = para.str.to_s
|
|
140
|
+
layout.context.resolution = dpi
|
|
140
141
|
if para.markup
|
|
141
142
|
para.str = @deck.typographer.process(layout.text)
|
|
142
|
-
layout.markup = para.str
|
|
143
|
+
layout.markup = para.str.to_s
|
|
143
144
|
end
|
|
144
145
|
|
|
145
146
|
set_font_rendering_opts!(layout)
|
data/lib/squib/layout_parser.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Squib
|
|
|
34
34
|
|
|
35
35
|
# Determine the file path of the built-in layout file
|
|
36
36
|
def builtin(file)
|
|
37
|
-
"#{File.dirname(__FILE__)}/layouts/#{file}"
|
|
37
|
+
"#{File.dirname(__FILE__)}/builtin/layouts/#{file}"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Process the extends recursively
|
|
@@ -53,6 +53,10 @@ module Squib
|
|
|
53
53
|
add_parent_child(parent_val, child_val)
|
|
54
54
|
elsif child_val.to_s.strip.start_with?('-=')
|
|
55
55
|
sub_parent_child(parent_val, child_val)
|
|
56
|
+
elsif child_val.to_s.strip.start_with?('*=')
|
|
57
|
+
mul_parent_child(parent_val, child_val)
|
|
58
|
+
elsif child_val.to_s.strip.start_with?('/=')
|
|
59
|
+
div_parent_child(parent_val, child_val)
|
|
56
60
|
else
|
|
57
61
|
child_val # child overrides parent when merging, no +=
|
|
58
62
|
end
|
|
@@ -76,6 +80,18 @@ module Squib
|
|
|
76
80
|
parent_pixels - child_pixels
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
def mul_parent_child(parent, child)
|
|
84
|
+
parent_pixels = Args::UnitConversion.parse(parent, @dpi).to_f
|
|
85
|
+
child_float = child.sub('*=', '').to_f
|
|
86
|
+
parent_pixels * child_float
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def div_parent_child(parent, child)
|
|
90
|
+
parent_pixels = Args::UnitConversion.parse(parent, @dpi).to_f
|
|
91
|
+
child_float = child.sub('/=', '').to_f
|
|
92
|
+
parent_pixels / child_float
|
|
93
|
+
end
|
|
94
|
+
|
|
79
95
|
# Does this layout entry have an extends field?
|
|
80
96
|
# i.e. is it a base-case or will it need recursion?
|
|
81
97
|
# :nodoc:
|
data/lib/squib/sample_helpers.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Squib
|
|
2
|
+
module Sprues
|
|
3
|
+
class CropLine
|
|
4
|
+
attr_reader :x1, :y1, :x2, :y2
|
|
5
|
+
|
|
6
|
+
def initialize(type, position, sheet_width, sheet_height, dpi)
|
|
7
|
+
method = "parse_#{type}"
|
|
8
|
+
send method, position, sheet_width, sheet_height, dpi
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse_horizontal(position, sheet_width, _, dpi)
|
|
12
|
+
position = Args::UnitConversion.parse(position, dpi)
|
|
13
|
+
@x1 = 0
|
|
14
|
+
@y1 = position
|
|
15
|
+
@x2 = sheet_width
|
|
16
|
+
@y2 = position
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_vertical(position, _, sheet_height, dpi)
|
|
20
|
+
position = Args::UnitConversion.parse(position, dpi)
|
|
21
|
+
@x1 = position
|
|
22
|
+
@y1 = 0
|
|
23
|
+
@x2 = position
|
|
24
|
+
@y2 = sheet_height
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Squib
|
|
2
|
+
module Sprues
|
|
3
|
+
class CropLineDash
|
|
4
|
+
VALIDATION_REGEX = /%r{
|
|
5
|
+
^(\d*[.])?\d+(in|cm|mm)
|
|
6
|
+
\s+
|
|
7
|
+
(\d*[.])?\d+(in|cm|mm)$
|
|
8
|
+
}x/
|
|
9
|
+
|
|
10
|
+
attr_reader :pattern
|
|
11
|
+
|
|
12
|
+
def initialize(value, dpi)
|
|
13
|
+
if value == :solid
|
|
14
|
+
@pattern = nil
|
|
15
|
+
elsif value == :dotted
|
|
16
|
+
@pattern = [
|
|
17
|
+
Args::UnitConversion.parse('0.2mm', dpi),
|
|
18
|
+
Args::UnitConversion.parse('0.5mm', dpi)
|
|
19
|
+
]
|
|
20
|
+
elsif value == :dashed
|
|
21
|
+
@pattern = [
|
|
22
|
+
Args::UnitConversion.parse('2mm', dpi),
|
|
23
|
+
Args::UnitConversion.parse('2mm', dpi)
|
|
24
|
+
]
|
|
25
|
+
elsif value.is_a? String
|
|
26
|
+
@pattern = value.split(' ').map do |val|
|
|
27
|
+
Args::UnitConversion.parse val, dpi
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
raise ArgumentError, 'Unsupported dash style'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'classy_hash'
|
|
3
|
+
require_relative '../args/color_validator'
|
|
4
|
+
require_relative '../args/unit_conversion'
|
|
5
|
+
require_relative 'crop_line'
|
|
6
|
+
require_relative 'crop_line_dash'
|
|
7
|
+
require_relative 'invalid_sprue_definition'
|
|
8
|
+
require_relative 'sprue_schema'
|
|
9
|
+
|
|
10
|
+
module Squib
|
|
11
|
+
class Sprue
|
|
12
|
+
include Args::ColorValidator
|
|
13
|
+
|
|
14
|
+
# Defaults are set for poker sized deck on a A4 sheet, with no cards
|
|
15
|
+
DEFAULTS = {
|
|
16
|
+
'sheet_width' => nil,
|
|
17
|
+
'sheet_height' => nil,
|
|
18
|
+
'card_width' => nil,
|
|
19
|
+
'card_height' => nil,
|
|
20
|
+
'dpi' => 300,
|
|
21
|
+
'position_reference' => :topleft,
|
|
22
|
+
'rotate' => 0.0,
|
|
23
|
+
'crop_line' => {
|
|
24
|
+
'style' => :solid,
|
|
25
|
+
'width' => '0.02mm',
|
|
26
|
+
'color' => :black,
|
|
27
|
+
'overlay' => :on_margin,
|
|
28
|
+
'lines' => []
|
|
29
|
+
},
|
|
30
|
+
'cards' => []
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :dpi
|
|
34
|
+
|
|
35
|
+
def initialize(template_hash, dpi)
|
|
36
|
+
@template_hash = template_hash
|
|
37
|
+
@dpi = dpi
|
|
38
|
+
@crop_line_default = @template_hash['crop_line'].select do |k, _|
|
|
39
|
+
%w[style width color].include? k
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load the template definition file
|
|
44
|
+
def self.load(file, dpi)
|
|
45
|
+
yaml = {}
|
|
46
|
+
thefile = file if File.exist?(file) # use custom first
|
|
47
|
+
thefile = builtin(file) if File.exist?(builtin(file)) # then builtin
|
|
48
|
+
unless File.exist?(thefile)
|
|
49
|
+
Squib::logger.error("Sprue not found: #{file}. Falling back to defaults.")
|
|
50
|
+
end
|
|
51
|
+
yaml = YAML.load_file(thefile) || {} if File.exist? thefile
|
|
52
|
+
# Bake the default values into our sprue
|
|
53
|
+
new_hash = DEFAULTS.merge(yaml)
|
|
54
|
+
new_hash['crop_line'] = DEFAULTS['crop_line'].
|
|
55
|
+
merge(new_hash['crop_line'])
|
|
56
|
+
warn_unrecognized(yaml)
|
|
57
|
+
|
|
58
|
+
# Validate
|
|
59
|
+
begin
|
|
60
|
+
require 'benchmark'
|
|
61
|
+
ClassyHash.validate(new_hash, Sprues::SCHEMA)
|
|
62
|
+
rescue ClassyHash::SchemaViolationError => e
|
|
63
|
+
raise Sprues::InvalidSprueDefinition.new(thefile, e)
|
|
64
|
+
end
|
|
65
|
+
Sprue.new new_hash, dpi
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sheet_width
|
|
69
|
+
Args::UnitConversion.parse @template_hash['sheet_width'], @dpi
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sheet_height
|
|
73
|
+
Args::UnitConversion.parse @template_hash['sheet_height'], @dpi
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def card_width
|
|
77
|
+
Args::UnitConversion.parse @template_hash['card_width'], @dpi
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def card_height
|
|
81
|
+
Args::UnitConversion.parse @template_hash['card_height'], @dpi
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def card_default_rotation
|
|
85
|
+
parse_rotate_param @template_hash['rotate']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def crop_line_overlay
|
|
89
|
+
@template_hash['crop_line']['overlay']
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def crop_lines
|
|
93
|
+
lines = @template_hash['crop_line']['lines'].map(
|
|
94
|
+
&method(:parse_crop_line)
|
|
95
|
+
)
|
|
96
|
+
if block_given?
|
|
97
|
+
lines.each { |v| yield v }
|
|
98
|
+
else
|
|
99
|
+
lines
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def cards
|
|
104
|
+
parsed_cards = @template_hash['cards'].map(&method(:parse_card))
|
|
105
|
+
if block_given?
|
|
106
|
+
parsed_cards.each { |v| yield v }
|
|
107
|
+
else
|
|
108
|
+
parsed_cards
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def margin
|
|
113
|
+
# NOTE: There's a baseline of 0.25mm that we can 100% make sure that we
|
|
114
|
+
# can overlap really thin lines on the PDF
|
|
115
|
+
crop_line_width = [
|
|
116
|
+
Args::UnitConversion.parse(@template_hash['crop_line']['width'], @dpi),
|
|
117
|
+
Args::UnitConversion.parse('0.25mm', @dpi)
|
|
118
|
+
].max
|
|
119
|
+
|
|
120
|
+
parsed_cards = cards
|
|
121
|
+
left, right = parsed_cards.minmax { |a, b| a['x'] <=> b['x'] }
|
|
122
|
+
top, bottom = parsed_cards.minmax { |a, b| a['y'] <=> b['y'] }
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
left: left['x'] - crop_line_width,
|
|
126
|
+
right: right['x'] + card_width + crop_line_width,
|
|
127
|
+
top: top['y'] - crop_line_width,
|
|
128
|
+
bottom: bottom['y'] + card_height + crop_line_width
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Warn unrecognized options in the template sheet
|
|
133
|
+
def self.warn_unrecognized(yaml)
|
|
134
|
+
unrec = yaml.keys - DEFAULTS.keys
|
|
135
|
+
return unless unrec.any?
|
|
136
|
+
|
|
137
|
+
Squib.logger.warn(
|
|
138
|
+
"Unrecognized configuration option(s): #{unrec.join(',')}"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Return path for built-in sheet templates
|
|
145
|
+
def self.builtin(file)
|
|
146
|
+
"#{File.dirname(__FILE__)}/../builtin/sprues/#{file}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Parse crop line definitions from template.
|
|
150
|
+
def parse_crop_line(line)
|
|
151
|
+
new_line = @crop_line_default.merge line
|
|
152
|
+
new_line['width'] = Args::UnitConversion.parse(new_line['width'], @dpi)
|
|
153
|
+
new_line['color'] = colorify new_line['color']
|
|
154
|
+
new_line['style_desc'] = new_line['style']
|
|
155
|
+
new_line['style'] = Sprues::CropLineDash.new(new_line['style'], @dpi)
|
|
156
|
+
new_line['line'] = Sprues::CropLine.new(
|
|
157
|
+
new_line['type'], new_line['position'], sheet_width, sheet_height, @dpi
|
|
158
|
+
)
|
|
159
|
+
new_line
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Parse card definitions from template.
|
|
163
|
+
def parse_card(card)
|
|
164
|
+
new_card = card.clone
|
|
165
|
+
|
|
166
|
+
x = Args::UnitConversion.parse(card['x'], @dpi)
|
|
167
|
+
y = Args::UnitConversion.parse(card['y'], @dpi)
|
|
168
|
+
if @template_hash['position_reference'] == :center
|
|
169
|
+
# Normalize it to a top-left positional reference
|
|
170
|
+
x -= card_width / 2
|
|
171
|
+
y -= card_height / 2
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
new_card['x'] = x
|
|
175
|
+
new_card['y'] = y
|
|
176
|
+
new_card['rotate'] = parse_rotate_param(
|
|
177
|
+
card['rotate'] ? card['rotate'] : @template_hash['rotate'])
|
|
178
|
+
new_card
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_rotate_param(val)
|
|
182
|
+
if val == :clockwise
|
|
183
|
+
0.5 * Math::PI
|
|
184
|
+
elsif val == :counterclockwise
|
|
185
|
+
1.5 * Math::PI
|
|
186
|
+
elsif val == :turnaround
|
|
187
|
+
Math::PI
|
|
188
|
+
elsif val.is_a? String
|
|
189
|
+
if val.end_with? 'deg'
|
|
190
|
+
val.gsub(/deg$/, '').to_f / 180 * Math::PI
|
|
191
|
+
elsif val.end_with? 'rad'
|
|
192
|
+
val.gsub(/rad$/, '').to_f
|
|
193
|
+
else
|
|
194
|
+
val.to_f
|
|
195
|
+
end
|
|
196
|
+
elsif val.nil?
|
|
197
|
+
0.0
|
|
198
|
+
else
|
|
199
|
+
val.to_f
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|