laser-cutter 1.0.3 → 2.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.
- checksums.yaml +5 -5
- data/.envrc +2 -0
- data/.github/workflows/lint.yml +22 -0
- data/.github/workflows/rspec.yml +22 -0
- data/.gitignore +5 -4
- data/.plans/001.00-dry-cli-migration/plan.md +29 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.rspec +1 -1
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +90 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +62 -0
- data/CONTRIBUTING.md +2 -4
- data/Gemfile +14 -0
- data/README.md +83 -111
- data/Rakefile +41 -0
- data/docs/badges/coverage_badge.svg +21 -0
- data/exe/laser-cutter +8 -0
- data/exe/lc +1 -0
- data/justfile +97 -0
- data/laser-cutter.gemspec +26 -14
- data/lib/laser/cutter/aggregator.rb +101 -0
- data/lib/{laser-cutter → laser/cutter}/box.rb +44 -52
- data/lib/laser/cutter/cli/command.rb +46 -0
- data/lib/laser/cutter/cli/completion.rb +21 -0
- data/lib/laser/cutter/cli/config_file.rb +36 -0
- data/lib/laser/cutter/cli/examples.rb +42 -0
- data/lib/laser/cutter/cli/generate.rb +90 -0
- data/lib/laser/cutter/cli/help.rb +20 -0
- data/lib/laser/cutter/cli/page_sizes.rb +19 -0
- data/lib/laser/cutter/cli/version.rb +15 -0
- data/lib/laser/cutter/cli.rb +49 -0
- data/lib/laser/cutter/configuration.rb +101 -0
- data/lib/{laser-cutter → laser/cutter}/geometry/dimensions.rb +2 -3
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/line.rb +19 -18
- data/lib/{laser-cutter → laser/cutter}/geometry/point.rb +4 -2
- data/lib/{laser-cutter/geometry/shape → laser/cutter/geometry}/rect.rb +6 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/shape.rb +7 -7
- data/lib/{laser-cutter → laser/cutter}/geometry/tuple.rb +40 -36
- data/lib/laser/cutter/launcher.rb +40 -0
- data/lib/laser/cutter/missing_option.rb +7 -0
- data/lib/laser/cutter/notching/base.rb +19 -0
- data/lib/{laser-cutter → laser/cutter}/notching/edge.rb +21 -20
- data/lib/laser/cutter/notching/infinite_iterator.rb +27 -0
- data/lib/{laser-cutter → laser/cutter}/notching/path_generator.rb +27 -60
- data/lib/laser/cutter/notching/shift.rb +17 -0
- data/lib/laser/cutter/page_manager.rb +50 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/base.rb +3 -2
- data/lib/laser/cutter/renderer/box_renderer.rb +40 -0
- data/lib/laser/cutter/renderer/layout_renderer.rb +61 -0
- data/lib/laser/cutter/renderer/line_renderer.rb +21 -0
- data/lib/laser/cutter/renderer/meta_renderer.rb +83 -0
- data/lib/{laser-cutter → laser/cutter}/renderer/rect_renderer.rb +3 -1
- data/lib/laser/cutter/renderer/svg_renderer.rb +65 -0
- data/lib/laser/cutter/renderer.rb +24 -0
- data/lib/laser/cutter/units_converter.rb +15 -0
- data/lib/laser/cutter/version.rb +7 -0
- data/lib/laser/cutter/zero_value_not_allowed.rb +7 -0
- data/lib/laser/cutter.rb +29 -0
- data/lib/laser-cutter.rb +2 -14
- data/lib/laser_cutter.rb +3 -0
- data/spec/laser/cutter/aggregator_spec.rb +70 -0
- data/spec/laser/cutter/box_spec.rb +61 -0
- data/spec/laser/cutter/cli/config_file_spec.rb +31 -0
- data/spec/laser/cutter/cli_spec.rb +186 -0
- data/spec/laser/cutter/configuration_spec.rb +101 -0
- data/spec/{dimensions_spec.rb → laser/cutter/dimensions_spec.rb} +8 -6
- data/spec/laser/cutter/edge_spec.rb +64 -0
- data/spec/{line_spec.rb → laser/cutter/line_spec.rb} +26 -20
- data/spec/{page_manager_spec.rb → laser/cutter/page_manager_spec.rb} +27 -11
- data/spec/{path_generator_spec.rb → laser/cutter/path_generator_spec.rb} +18 -14
- data/spec/{point_spec.rb → laser/cutter/point_spec.rb} +27 -22
- data/spec/{rect_spec.rb → laser/cutter/rect_spec.rb} +9 -5
- data/spec/laser/cutter/renderer/layout_renderer_spec.rb +102 -0
- data/spec/laser/cutter/renderer/svg_renderer_spec.rb +51 -0
- data/spec/laser/cutter/renderer_spec.rb +19 -0
- data/spec/spec_helper.rb +34 -25
- data/spec/support/aruba.rb +10 -0
- metadata +206 -80
- data/.travis.yml +0 -11
- data/LICENSE +0 -22
- data/bin/laser-cutter +0 -19
- data/lib/laser-cutter/aggregator.rb +0 -57
- data/lib/laser-cutter/cli/opt_parser.rb +0 -131
- data/lib/laser-cutter/cli/serializer.rb +0 -51
- data/lib/laser-cutter/configuration.rb +0 -83
- data/lib/laser-cutter/geometry.rb +0 -11
- data/lib/laser-cutter/notching/base.rb +0 -13
- data/lib/laser-cutter/notching.rb +0 -10
- data/lib/laser-cutter/page_manager.rb +0 -43
- data/lib/laser-cutter/renderer/box_renderer.rb +0 -36
- data/lib/laser-cutter/renderer/layout_renderer.rb +0 -59
- data/lib/laser-cutter/renderer/line_renderer.rb +0 -17
- data/lib/laser-cutter/renderer/meta_renderer.rb +0 -77
- data/lib/laser-cutter/renderer.rb +0 -14
- data/lib/laser-cutter/version.rb +0 -5
- data/spec/aggregator_spec.rb +0 -65
- data/spec/box_spec.rb +0 -40
- data/spec/configuration_spec.rb +0 -81
- data/spec/edge_spec.rb +0 -43
- data/spec/renderer_spec.rb +0 -72
- /data/{doc → docs/images}/comparison.jpg +0 -0
|
@@ -1,40 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Laser
|
|
3
4
|
module Cutter
|
|
4
5
|
module Notching
|
|
5
|
-
class Shift < Struct.new(:delta, :direction, :dim_index)
|
|
6
|
-
def next_point_after point
|
|
7
|
-
p = point.clone
|
|
8
|
-
shift = []
|
|
9
|
-
shift[dim_index] = delta * direction
|
|
10
|
-
shift[(dim_index + 1) % 2] = 0
|
|
11
|
-
p.plus *shift
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Alternating iterator
|
|
16
|
-
class InfiniteIterator < Struct.new(:array)
|
|
17
|
-
attr_accessor :array, :next_index, :calls
|
|
18
|
-
|
|
19
|
-
def initialize(array)
|
|
20
|
-
self.array = array
|
|
21
|
-
self.calls = 0
|
|
22
|
-
self.next_index = 0
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def next
|
|
26
|
-
item = self.array[next_index].clone
|
|
27
|
-
self.next_index += 1
|
|
28
|
-
self.next_index %= array.size
|
|
29
|
-
self.calls += 1
|
|
30
|
-
item = yield item, self.calls if block_given?
|
|
31
|
-
item
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
6
|
class PathGenerator
|
|
36
|
-
|
|
37
7
|
extend ::Forwardable
|
|
8
|
+
|
|
38
9
|
%i(center_out thickness corners kerf kerf? notch_width first_notch_out? adjust_corners corners).each do |method_name|
|
|
39
10
|
def_delegator :@edge, method_name, method_name
|
|
40
11
|
end
|
|
@@ -66,7 +37,7 @@ module Laser
|
|
|
66
37
|
point = starting_point
|
|
67
38
|
|
|
68
39
|
vertices << point
|
|
69
|
-
adjust_for_kerf(vertices
|
|
40
|
+
adjust_for_kerf(vertices, -1) if adjust_corners && !first_notch_out?
|
|
70
41
|
shifts.each do |shift|
|
|
71
42
|
point = shift.next_point_after point
|
|
72
43
|
vertices << point
|
|
@@ -77,17 +48,16 @@ module Laser
|
|
|
77
48
|
end
|
|
78
49
|
|
|
79
50
|
def adjust_for_kerf(vertices, direction)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
51
|
+
return unless kerf?
|
|
52
|
+
|
|
53
|
+
point = vertices.pop
|
|
54
|
+
point = point.plus(2 * direction * shift_vector(1)) if corners
|
|
55
|
+
vertices << point
|
|
85
56
|
end
|
|
86
57
|
|
|
87
58
|
def corner_box_sides
|
|
88
59
|
boxes = []
|
|
89
60
|
extra_lines = []
|
|
90
|
-
sides = []
|
|
91
61
|
|
|
92
62
|
# These two boxes occupy the corners of the 3D box. They do not match
|
|
93
63
|
# in width to our notches because they are usually merged with them.
|
|
@@ -95,19 +65,17 @@ module Laser
|
|
|
95
65
|
boxes << Geometry::Rect[edge.inside.p1.clone, edge.outside.p1.clone]
|
|
96
66
|
boxes << Geometry::Rect[edge.inside.p2.clone, edge.outside.p2.clone]
|
|
97
67
|
|
|
98
|
-
if kerf?
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
extra_lines << add_boxes_when_in(dim_index, direction, k)
|
|
110
|
-
end
|
|
68
|
+
if kerf? && adjust_corners
|
|
69
|
+
if first_notch_out?
|
|
70
|
+
k = 2
|
|
71
|
+
direction = -1
|
|
72
|
+
dim_index = 1
|
|
73
|
+
extra_lines << add_corners_when_out(dim_index, direction, k)
|
|
74
|
+
else
|
|
75
|
+
k = -2
|
|
76
|
+
direction = 1
|
|
77
|
+
dim_index = 0
|
|
78
|
+
extra_lines << add_boxes_when_in(dim_index, direction, k)
|
|
111
79
|
end
|
|
112
80
|
end
|
|
113
81
|
sides = boxes.flatten.map(&:relocate!).map(&:sides)
|
|
@@ -162,18 +130,17 @@ module Laser
|
|
|
162
130
|
def shift_vector(index, dim_shift = 0)
|
|
163
131
|
shift = []
|
|
164
132
|
shift[(d_index_across + dim_shift) % 2] = 0
|
|
165
|
-
shift[(d_index_along + dim_shift) % 2] = kerf / 2.0 * edge.send("v#{index}"
|
|
133
|
+
shift[(d_index_along + dim_shift) % 2] = kerf / 2.0 * edge.send(:"v#{index}").[]((d_index_along + dim_shift) % 2)
|
|
166
134
|
Vector.[](*shift)
|
|
167
135
|
end
|
|
168
136
|
|
|
169
|
-
|
|
170
137
|
def starting_point
|
|
171
138
|
edge.inside.p1.clone # start
|
|
172
139
|
end
|
|
173
140
|
|
|
174
141
|
# 0 = X, 1 = Y
|
|
175
142
|
def d_index_along
|
|
176
|
-
|
|
143
|
+
edge.inside.p1.x == edge.inside.p2.x ? 1 : 0
|
|
177
144
|
end
|
|
178
145
|
|
|
179
146
|
def d_index_across
|
|
@@ -181,11 +148,11 @@ module Laser
|
|
|
181
148
|
end
|
|
182
149
|
|
|
183
150
|
def direction_along
|
|
184
|
-
|
|
151
|
+
edge.inside.p1.coords.[](d_index_along) < edge.inside.p2.coords.[](d_index_along) ? 1 : -1
|
|
185
152
|
end
|
|
186
153
|
|
|
187
154
|
def direction_across
|
|
188
|
-
|
|
155
|
+
edge.inside.p1.coords.[](d_index_across) < edge.outside.p1.coords.[](d_index_across) ? 1 : -1
|
|
189
156
|
end
|
|
190
157
|
|
|
191
158
|
private
|
|
@@ -206,8 +173,8 @@ module Laser
|
|
|
206
173
|
end
|
|
207
174
|
|
|
208
175
|
(1..edge.notch_count).to_a.each do |notch_number|
|
|
209
|
-
shifts << along_iter.next do |shift,
|
|
210
|
-
if inner &&
|
|
176
|
+
shifts << along_iter.next do |shift, _index|
|
|
177
|
+
if inner && notch_number > 1 && notch_number < edge.notch_count
|
|
211
178
|
shift.delta -= kerf
|
|
212
179
|
elsif !inner
|
|
213
180
|
shift.delta += kerf
|
|
@@ -238,7 +205,7 @@ module Laser
|
|
|
238
205
|
lines = []
|
|
239
206
|
vertices.each_with_index do |v, i|
|
|
240
207
|
if v != vertices.last
|
|
241
|
-
lines << Geometry::Line.new(v, vertices[i+1])
|
|
208
|
+
lines << Geometry::Line.new(v, vertices[i + 1])
|
|
242
209
|
end
|
|
243
210
|
end
|
|
244
211
|
lines.flatten
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Notching
|
|
6
|
+
Shift = Struct.new(:delta, :direction, :dim_index) do
|
|
7
|
+
def next_point_after(point)
|
|
8
|
+
p = point.clone
|
|
9
|
+
shift = []
|
|
10
|
+
shift[dim_index] = delta * direction
|
|
11
|
+
shift[(dim_index + 1) % 2] = 0
|
|
12
|
+
p.plus(*shift)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
class PageManager
|
|
6
|
+
SIZES = PDF::Core::PageGeometry::SIZES.clone.freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :units
|
|
9
|
+
|
|
10
|
+
# @param units [String, Symbol] 'in' or 'mm'; the configuration's default is a Symbol
|
|
11
|
+
def initialize(units)
|
|
12
|
+
@units = units.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_page_sizes
|
|
16
|
+
output = +""
|
|
17
|
+
page_size_values.each do |k|
|
|
18
|
+
output << sprintf("\t%10s:\t%6.1f x %6.1f\n", *k)
|
|
19
|
+
end
|
|
20
|
+
output
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# if from_units is nil, we expect it to be in dots per inch (default
|
|
24
|
+
# measurements for Prawn
|
|
25
|
+
def value_from_units(value, from_units = nil)
|
|
26
|
+
multiplier = if from_units.nil?
|
|
27
|
+
if units.eql?('in')
|
|
28
|
+
1.0 / 72.0 # PDF units per inch
|
|
29
|
+
else
|
|
30
|
+
25.4 * 1.0 / 72.0
|
|
31
|
+
end
|
|
32
|
+
elsif units.eql?(from_units)
|
|
33
|
+
1.0
|
|
34
|
+
elsif units.eql?('in') && from_units.eql?('mm')
|
|
35
|
+
(1.0 / 25.4)
|
|
36
|
+
else
|
|
37
|
+
25.4
|
|
38
|
+
end
|
|
39
|
+
value.to_f * multiplier
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def page_size_values
|
|
43
|
+
h = Hashie::Mash.new(SIZES)
|
|
44
|
+
h.keys.sort.map do |k|
|
|
45
|
+
[k, value_from_units(h[k][0].to_f), value_from_units(h[k][1].to_f)]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Renderer
|
|
6
|
+
class BoxRenderer < Base
|
|
7
|
+
alias_method :box, :subject
|
|
8
|
+
|
|
9
|
+
def initialize(config)
|
|
10
|
+
super
|
|
11
|
+
self.subject = Laser::Cutter::Box.new(config)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ensure_space_for(rect)
|
|
15
|
+
coords = [rect.p2.x, rect.p2.y].map{ |a| page_manager.value_from_units(a) }
|
|
16
|
+
box.metadata = Geometry::Point.new(coords)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enclosure
|
|
20
|
+
box.enclosure
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array<Geometry::Line>] every line to cut, computed once
|
|
24
|
+
def lines
|
|
25
|
+
@lines ||= box.generate_notches
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Draws the box, yielding after each line so a caller can count them.
|
|
29
|
+
def render(pdf = nil)
|
|
30
|
+
pdf.line_width = config.stroke.send(units)
|
|
31
|
+
pdf.stroke_color config[:color] || BLACK
|
|
32
|
+
lines.each do |line|
|
|
33
|
+
LineRenderer.new(config, line).render(pdf)
|
|
34
|
+
yield line if block_given?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Renderer
|
|
6
|
+
# Writes the PDF: the box, its metadata block, and the unkerfed outline
|
|
7
|
+
# when asked for.
|
|
8
|
+
class LayoutRenderer < Base
|
|
9
|
+
UNKERFED_COLOR = 'DD2211'
|
|
10
|
+
|
|
11
|
+
def initialize(config)
|
|
12
|
+
self.config = config
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer] how many lines {#render} draws, and yields
|
|
17
|
+
def total
|
|
18
|
+
box_renderers.sum { |renderer| renderer.lines.size }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Writes the file, yielding after each line drawn.
|
|
22
|
+
def render(&)
|
|
23
|
+
margin = config.margin.to_f.send(units)
|
|
24
|
+
pdf = Prawn::Document.new(margin: margin,
|
|
25
|
+
page_size: config.page_size || calculate_image_boundary(box_renderers.first, margin),
|
|
26
|
+
page_layout: config.page_layout.to_sym)
|
|
27
|
+
|
|
28
|
+
box_renderers.first.render(pdf, &)
|
|
29
|
+
meta_renderer&.render(pdf)
|
|
30
|
+
box_renderers.drop(1).each { |renderer| renderer.render(pdf, &) }
|
|
31
|
+
pdf.render_file(config.file)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def calculate_image_boundary(box_renderer, margin)
|
|
35
|
+
box_renderer.enclosure.to_a[1].map do |c|
|
|
36
|
+
c.send(units) + (2 * margin)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def meta_renderer
|
|
43
|
+
@meta_renderer ||= MetaRenderer.new(config) if config.metadata
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The box, then the same box without kerf when config.debug is set.
|
|
47
|
+
def box_renderers
|
|
48
|
+
@box_renderers ||= begin
|
|
49
|
+
configs = [config]
|
|
50
|
+
configs << Configuration.new(config.to_hash).merge!(kerf: 0.0, color: UNKERFED_COLOR) if config.debug
|
|
51
|
+
configs.map do |box_config|
|
|
52
|
+
BoxRenderer.new(box_config).tap do |renderer|
|
|
53
|
+
renderer.ensure_space_for(meta_renderer.enclosure) if meta_renderer
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Renderer
|
|
6
|
+
class LineRenderer < Base
|
|
7
|
+
alias_method :line, :subject
|
|
8
|
+
def render(pdf = nil)
|
|
9
|
+
pdf.stroke {
|
|
10
|
+
pdf.line([line.p1.x, line.p1.y].map{ |p| p.send(units) },
|
|
11
|
+
[line.p2.x, line.p2.y].map{ |p| p.send(units) })
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enclosure
|
|
16
|
+
line
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Renderer
|
|
6
|
+
class MetaRenderer < Laser::Cutter::Renderer::Base
|
|
7
|
+
META_KEYS = %i(units width height depth thickness notch kerf stroke padding margin page_size page_layout).freeze
|
|
8
|
+
|
|
9
|
+
def initialize(config = {})
|
|
10
|
+
self.config = config
|
|
11
|
+
self.enclosure = Laser::Cutter::Geometry::Rect.create(Laser::Cutter::Geometry::Point[1, 1], 140, 150)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(pdf = nil)
|
|
15
|
+
banner = <<-EOF
|
|
16
|
+
|
|
17
|
+
Made with Laser Cutter Ruby Gem (v#{Laser::Cutter::VERSION})
|
|
18
|
+
Credits to Prawn for ruby PDF generation,
|
|
19
|
+
and BoxMaker for inspiration.
|
|
20
|
+
|
|
21
|
+
Online: http://makeabox.io/
|
|
22
|
+
Source: https://github.com/kigster/laser-cutter
|
|
23
|
+
EOF
|
|
24
|
+
|
|
25
|
+
meta_color = BLUE
|
|
26
|
+
meta_top_height = 55
|
|
27
|
+
|
|
28
|
+
metadata = config.to_hash
|
|
29
|
+
metadata[:page_size] ||= 'custom'
|
|
30
|
+
metadata.delete(:page_layout) if metadata[:page_size].eql?('custom')
|
|
31
|
+
|
|
32
|
+
meta_fields = META_KEYS.find_all{ |k| metadata[k] }.join(": \n") + ": \n"
|
|
33
|
+
meta_values = META_KEYS.find_all{ |k| metadata[k] }.map{ |k| metadata[k].to_s }.join("\n")
|
|
34
|
+
|
|
35
|
+
rect = enclosure
|
|
36
|
+
|
|
37
|
+
pdf.instance_eval do
|
|
38
|
+
self.line_width = 0.2.mm
|
|
39
|
+
float do
|
|
40
|
+
bounding_box([rect.p1.x, rect.h + rect.p1.y], width: rect.w, height: rect.h) do
|
|
41
|
+
stroke_color meta_color
|
|
42
|
+
stroke_bounds
|
|
43
|
+
|
|
44
|
+
# Print banner
|
|
45
|
+
indent 10 do
|
|
46
|
+
font('Helvetica', size: 6) do
|
|
47
|
+
text banner, color: meta_color
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# print values of the config, in two parts – keys right aligned first, values left aligned second.
|
|
52
|
+
float do
|
|
53
|
+
bounding_box([0, rect.h - meta_top_height],
|
|
54
|
+
width: rect.w,
|
|
55
|
+
height: rect.h - meta_top_height) do
|
|
56
|
+
float do
|
|
57
|
+
bounding_box([0, rect.h - meta_top_height], width: 70, height: rect.h - meta_top_height) do
|
|
58
|
+
indent 10 do
|
|
59
|
+
font('Helvetica', size: 7) do
|
|
60
|
+
text meta_fields, color: meta_color, align: :right
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
float do
|
|
66
|
+
bounding_box([60, rect.h - meta_top_height], width: 70, height: rect.h - meta_top_height) do
|
|
67
|
+
indent 10 do
|
|
68
|
+
font('Helvetica', size: 7) do
|
|
69
|
+
text meta_values, color: meta_color
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'victor'
|
|
4
|
+
|
|
5
|
+
module Laser
|
|
6
|
+
module Cutter
|
|
7
|
+
module Renderer
|
|
8
|
+
# Writes the box as an SVG sized in the configuration's own units. It
|
|
9
|
+
# fits the page to the box, and carries the metadata as a <desc>
|
|
10
|
+
# instead of drawing it.
|
|
11
|
+
class SvgRenderer < Base
|
|
12
|
+
def initialize(config)
|
|
13
|
+
super
|
|
14
|
+
self.subject = Laser::Cutter::Box.new(config)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Array<Geometry::Line>] every line to cut, computed once
|
|
18
|
+
def lines
|
|
19
|
+
@lines ||= subject.generate_notches
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] how many lines {#render} draws, and yields
|
|
23
|
+
def total
|
|
24
|
+
lines.size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Writes the file, yielding after each line drawn.
|
|
28
|
+
def render
|
|
29
|
+
svg = Victor::SVG.new(width: "#{width}#{units}", height: "#{height}#{units}", viewBox: "0 0 #{width} #{height}")
|
|
30
|
+
svg.element(:desc, description) if config.metadata
|
|
31
|
+
lines.each do |line|
|
|
32
|
+
svg.line(**coordinates(line), stroke: "##{config[:color] || BLACK}", stroke_width: config.stroke)
|
|
33
|
+
yield line if block_given?
|
|
34
|
+
end
|
|
35
|
+
File.write(config.file, svg.render)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def margin
|
|
41
|
+
config.margin.to_f
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def width
|
|
45
|
+
(subject.enclosure.p2.x + (2 * margin)).round(5)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def height
|
|
49
|
+
(subject.enclosure.p2.y + (2 * margin)).round(5)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# SVG counts y down from the top, the box geometry up from the bottom.
|
|
53
|
+
def coordinates(line)
|
|
54
|
+
{ x1: (line.p1.x + margin).round(5), y1: (height - margin - line.p1.y).round(5),
|
|
55
|
+
x2: (line.p2.x + margin).round(5), y2: (height - margin - line.p2.y).round(5) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def description
|
|
59
|
+
fields = MetaRenderer::META_KEYS.filter_map { |key| "#{key}: #{config[key]}" if config[key] }
|
|
60
|
+
"Made with laser-cutter v#{VERSION}. #{fields.join(', ')}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laser
|
|
4
|
+
module Cutter
|
|
5
|
+
module Renderer
|
|
6
|
+
# The renderer that writes each output format.
|
|
7
|
+
FORMATS = {
|
|
8
|
+
'pdf' => 'LayoutRenderer',
|
|
9
|
+
'svg' => 'SvgRenderer'
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
# @param format [String, Symbol] 'pdf' or 'svg', in either case
|
|
13
|
+
# @param config [Configuration]
|
|
14
|
+
# @return [LayoutRenderer, SvgRenderer]
|
|
15
|
+
# @raise [Error] for a format nothing here writes
|
|
16
|
+
def self.for(format, config)
|
|
17
|
+
name = FORMATS.fetch(format.to_s.downcase) do
|
|
18
|
+
raise Error, "unknown format #{format.inspect}, expected one of: #{FORMATS.keys.join(', ')}"
|
|
19
|
+
end
|
|
20
|
+
const_get(name).new(config)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/laser/cutter.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require "json"
|
|
5
|
+
require "matrix"
|
|
6
|
+
|
|
7
|
+
require "hashie/mash"
|
|
8
|
+
require "hashie/extensions/symbolize_keys"
|
|
9
|
+
require "hashie/extensions/mash/symbolize_keys"
|
|
10
|
+
require "pdf/core/page_geometry"
|
|
11
|
+
require "prawn"
|
|
12
|
+
require "prawn/measurement_extensions"
|
|
13
|
+
require "dry/cli"
|
|
14
|
+
require "dry/cli/help"
|
|
15
|
+
require "dry/cli/ui"
|
|
16
|
+
require "dry/cli/autocomplete/command"
|
|
17
|
+
require "tty-screen"
|
|
18
|
+
require "zeitwerk"
|
|
19
|
+
|
|
20
|
+
module Laser
|
|
21
|
+
module Cutter
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
loader = Zeitwerk::Loader.for_gem_extension(Laser)
|
|
27
|
+
loader.inflector.inflect("cli" => "CLI")
|
|
28
|
+
loader.setup
|
|
29
|
+
loader.eager_load
|
data/lib/laser-cutter.rb
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
require 'laser-cutter/configuration'
|
|
3
|
-
require 'laser-cutter/geometry'
|
|
4
|
-
require 'laser-cutter/box'
|
|
5
|
-
require 'laser-cutter/renderer'
|
|
6
|
-
require 'laser-cutter/page_manager'
|
|
7
|
-
require 'laser-cutter/notching'
|
|
8
|
-
require 'laser-cutter/aggregator'
|
|
9
|
-
require 'prawn'
|
|
10
|
-
require 'prawn/measurement_extensions'
|
|
1
|
+
# frozen_string_literal: true
|
|
11
2
|
|
|
12
|
-
|
|
13
|
-
module Cutter
|
|
14
|
-
end
|
|
15
|
-
end
|
|
3
|
+
require_relative "laser/cutter"
|
data/lib/laser_cutter.rb
ADDED