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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -4
  3. data/CHANGELOG.md +19 -3
  4. data/README.md +1 -1
  5. data/bin/squib +1 -16
  6. data/lib/squib.rb +1 -1
  7. data/lib/squib/api/data.rb +29 -0
  8. data/lib/squib/api/save.rb +23 -2
  9. data/lib/squib/api/shapes.rb +37 -0
  10. data/lib/squib/api/text.rb +1 -1
  11. data/lib/squib/api/units.rb +5 -0
  12. data/lib/squib/args/arg_loader.rb +1 -1
  13. data/lib/squib/args/coords.rb +3 -1
  14. data/lib/squib/args/dir_validator.rb +1 -1
  15. data/lib/squib/args/sheet.rb +9 -2
  16. data/lib/squib/args/sprue_file.rb +44 -0
  17. data/lib/squib/args/unit_conversion.rb +2 -0
  18. data/lib/squib/{layouts → builtin/layouts}/economy.yml +5 -5
  19. data/lib/squib/{layouts → builtin/layouts}/fantasy.yml +4 -4
  20. data/lib/squib/{layouts → builtin/layouts}/hand.yml +0 -0
  21. data/lib/squib/builtin/layouts/party.yml +94 -0
  22. data/lib/squib/{layouts → builtin/layouts}/playing-card.yml +0 -0
  23. data/lib/squib/{layouts → builtin/layouts}/tuck_box.yml +1 -1
  24. data/lib/squib/{project_template → builtin/projects/advanced}/.gitignore +0 -0
  25. data/lib/squib/{project_template → builtin/projects/advanced}/ABOUT.md +0 -0
  26. data/lib/squib/builtin/projects/advanced/Gemfile +11 -0
  27. data/lib/squib/builtin/projects/advanced/Guardfile +21 -0
  28. data/lib/squib/{project_template → builtin/projects/advanced}/IDEAS.md +0 -0
  29. data/lib/squib/{project_template → builtin/projects/advanced}/PLAYTESTING.md +0 -0
  30. data/lib/squib/builtin/projects/advanced/Rakefile +27 -0
  31. data/lib/squib/{project_template → builtin/projects/advanced}/_output/gitkeep.txt +0 -0
  32. data/lib/squib/builtin/projects/advanced/config.yml +49 -0
  33. data/lib/squib/builtin/projects/advanced/data/game.xlsx +0 -0
  34. data/lib/squib/project_template/PNP NOTES.md b/data/lib/squib/builtin/projects/advanced/docs/PNP → NOTES.md +0 -0
  35. data/lib/squib/{project_template → builtin/projects/advanced/docs}/RULES.md +0 -0
  36. data/lib/squib/builtin/projects/advanced/img/example.svg +60 -0
  37. data/lib/squib/builtin/projects/advanced/layouts/deck.yml +27 -0
  38. data/lib/squib/builtin/projects/advanced/src/deck.rb +34 -0
  39. data/lib/squib/builtin/projects/advanced/src/version.rb +3 -0
  40. data/lib/squib/builtin/projects/basic/.gitignore +4 -0
  41. data/lib/squib/builtin/projects/basic/ABOUT.md +19 -0
  42. data/lib/squib/{project_template → builtin/projects/basic}/Gemfile +0 -0
  43. data/lib/squib/builtin/projects/basic/IDEAS.md +22 -0
  44. data/lib/squib/builtin/projects/basic/PLAYTESTING.md +26 -0
  45. data/lib/squib/builtin/projects/basic/PNP NOTES.md +4 -0
  46. data/lib/squib/builtin/projects/basic/RULES.md +21 -0
  47. data/lib/squib/{project_template → builtin/projects/basic}/Rakefile +0 -0
  48. data/lib/squib/builtin/projects/basic/_output/gitkeep.txt +1 -0
  49. data/lib/squib/{project_template → builtin/projects/basic}/config.yml +0 -0
  50. data/lib/squib/{project_template → builtin/projects/basic}/deck.rb +0 -0
  51. data/lib/squib/{project_template → builtin/projects/basic}/layout.yml +0 -0
  52. data/lib/squib/builtin/sprues/a4_euro_card.yml +42 -0
  53. data/lib/squib/builtin/sprues/a4_poker_card_8up.yml +40 -0
  54. data/lib/squib/builtin/sprues/a4_poker_card_9up.yml +42 -0
  55. data/lib/squib/builtin/sprues/a4_usa_card.yml +42 -0
  56. data/lib/squib/builtin/sprues/letter_poker_card_9up.yml +25 -0
  57. data/lib/squib/builtin/sprues/letter_poker_foldable_8up.yml +52 -0
  58. data/lib/squib/card.rb +1 -1
  59. data/lib/squib/commands/cli.rb +39 -0
  60. data/lib/squib/commands/data/template_option.rb +109 -0
  61. data/lib/squib/commands/make_sprue.rb +275 -0
  62. data/lib/squib/commands/new.rb +37 -4
  63. data/lib/squib/constants.rb +6 -1
  64. data/lib/squib/graphics/cairo_context_wrapper.rb +1 -1
  65. data/lib/squib/graphics/image.rb +6 -1
  66. data/lib/squib/graphics/save_doc.rb +6 -4
  67. data/lib/squib/graphics/save_pdf.rb +13 -8
  68. data/lib/squib/graphics/save_sprue.rb +228 -0
  69. data/lib/squib/graphics/shapes.rb +8 -2
  70. data/lib/squib/graphics/text.rb +4 -3
  71. data/lib/squib/layout_parser.rb +17 -1
  72. data/lib/squib/sample_helpers.rb +1 -1
  73. data/lib/squib/sprues/crop_line.rb +28 -0
  74. data/lib/squib/sprues/crop_line_dash.rb +35 -0
  75. data/lib/squib/sprues/invalid_sprue_definition.rb +9 -0
  76. data/lib/squib/sprues/sprue.rb +203 -0
  77. data/lib/squib/sprues/sprue_schema.rb +48 -0
  78. data/lib/squib/version.rb +1 -1
  79. data/samples/autoscale_font/_autoscale_font.rb +3 -3
  80. data/samples/backend/_backend.rb +1 -1
  81. data/samples/basic.rb +2 -2
  82. data/samples/colors/_colors.rb +1 -1
  83. data/samples/colors/_gradients.rb +1 -1
  84. data/samples/config/config_text_markup.rb +3 -3
  85. data/samples/config/custom_config.rb +1 -1
  86. data/samples/data/_csv.rb +3 -3
  87. data/samples/data/_excel.rb +4 -4
  88. data/samples/data/_yaml.rb +12 -0
  89. data/samples/images/_images.rb +2 -2
  90. data/samples/images/_more_load_images.rb +3 -0
  91. data/samples/images/_unicode.rb +2 -2
  92. data/samples/intro/02_options.rb +2 -2
  93. data/samples/layouts/builtin_layouts.rb +27 -4
  94. data/samples/layouts/layouts.rb +2 -2
  95. data/samples/proofs/_tgc_proofs.rb +2 -2
  96. data/samples/ranges/_ranges.rb +3 -3
  97. data/samples/saves/_hand.rb +1 -1
  98. data/samples/saves/_save_pdf.rb +4 -0
  99. data/samples/saves/_saves.rb +7 -1
  100. data/samples/saves/_showcase.rb +1 -1
  101. data/samples/shapes/_draw_shapes.rb +5 -1
  102. data/samples/shapes/_proofs.rb +22 -0
  103. data/samples/sprues/_builtin_sprues.rb +19 -0
  104. data/samples/sprues/_fold_sheet.rb +27 -0
  105. data/samples/sprues/_hex_tiles.rb +15 -0
  106. data/samples/sprues/_mints.rb +11 -0
  107. data/samples/sprues/_sprue_example.rb +11 -0
  108. data/samples/text/_embed_text.rb +14 -14
  109. data/samples/text/_text.rb +8 -8
  110. data/samples/text/_text_options.rb +17 -17
  111. data/squib.gemspec +18 -17
  112. 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
- cc.move_to(x + r, y)
22
- cc.circle(x, y, r)
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
@@ -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)
@@ -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:
@@ -24,7 +24,7 @@ module Squib
24
24
  text str: str, x: 460, y: @sample_y - 40,
25
25
  width: 540, height: 180,
26
26
  valign: 'middle', align: 'center',
27
- font: 'Times New Roman,Serif 24'
27
+ font: 'Times New Roman,Serif 8'
28
28
  yield @sample_x, @sample_y
29
29
  @sample_y += 200
30
30
  end
@@ -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,9 @@
1
+ module Squib
2
+ module Sprues
3
+ class InvalidSprueDefinition < StandardError
4
+ def initialize(file, error)
5
+ super("Invalid sprue definition in file: #{file}. #{error.message}")
6
+ end
7
+ end
8
+ end
9
+ 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