laser-cutter 0.5.3 → 1.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 +4 -4
- data/.travis.yml +1 -1
- data/README.md +20 -16
- data/bin/laser-cutter +4 -158
- data/lib/laser-cutter.rb +2 -0
- data/lib/laser-cutter/aggregator.rb +57 -0
- data/lib/laser-cutter/box.rb +37 -24
- data/lib/laser-cutter/cli/opt_parser.rb +131 -0
- data/lib/laser-cutter/cli/serializer.rb +51 -0
- data/lib/laser-cutter/configuration.rb +3 -2
- data/lib/laser-cutter/geometry.rb +0 -3
- data/lib/laser-cutter/geometry/dimensions.rb +3 -3
- data/lib/laser-cutter/geometry/point.rb +0 -46
- data/lib/laser-cutter/geometry/shape/line.rb +40 -1
- data/lib/laser-cutter/geometry/shape/rect.rb +2 -2
- data/lib/laser-cutter/geometry/tuple.rb +73 -27
- data/lib/laser-cutter/notching.rb +10 -0
- data/lib/laser-cutter/notching/base.rb +13 -0
- data/lib/laser-cutter/notching/edge.rb +87 -0
- data/lib/laser-cutter/notching/path_generator.rb +249 -0
- data/lib/laser-cutter/renderer/base.rb +1 -1
- data/lib/laser-cutter/renderer/box_renderer.rb +2 -2
- data/lib/laser-cutter/renderer/layout_renderer.rb +19 -8
- data/lib/laser-cutter/renderer/meta_renderer.rb +8 -9
- data/lib/laser-cutter/version.rb +1 -1
- data/spec/aggregator_spec.rb +65 -0
- data/spec/box_spec.rb +5 -1
- data/spec/dimensions_spec.rb +0 -1
- data/spec/edge_spec.rb +43 -0
- data/spec/line_spec.rb +42 -19
- data/spec/path_generator_spec.rb +30 -36
- data/spec/point_spec.rb +2 -2
- data/spec/rect_spec.rb +1 -1
- data/spec/renderer_spec.rb +14 -5
- metadata +13 -5
- data/lib/laser-cutter/geometry/edge.rb +0 -33
- data/lib/laser-cutter/geometry/notched_path.rb +0 -46
- data/lib/laser-cutter/geometry/path_generator.rb +0 -129
@@ -0,0 +1,87 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Notching
|
4
|
+
MINIMUM_NOTCHES_PER_SIDE = 3
|
5
|
+
# This class represents a single edge of one side: both inside
|
6
|
+
# and outside edge of the material. It's also responsible
|
7
|
+
# for calculating the "perfect" notch width.
|
8
|
+
class Edge
|
9
|
+
|
10
|
+
attr_accessor :outside, :inside
|
11
|
+
attr_accessor :notch_width
|
12
|
+
attr_accessor :thickness, :kerf
|
13
|
+
attr_accessor :center_out, :corners, :adjust_corners
|
14
|
+
attr_accessor :notch_count, :v1, :v2
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(outside, inside, options = {})
|
18
|
+
self.outside = outside.clone
|
19
|
+
self.inside = inside.clone
|
20
|
+
|
21
|
+
# two vectors representing directions going from beginning of each inside line to the outside
|
22
|
+
self.v1 = [inside.p1.x - outside.p1.x, inside.p1.y - outside.p1.y].map{|e| -(e / e.abs).to_f }
|
23
|
+
self.v2 = [inside.p2.x - outside.p2.x, inside.p2.y - outside.p2.y].map{|e| -(e / e.abs).to_f }
|
24
|
+
|
25
|
+
self.v1 = Vector.[](*self.v1)
|
26
|
+
self.v2 = Vector.[](*self.v2)
|
27
|
+
|
28
|
+
self.center_out = options[:center_out] || false
|
29
|
+
self.thickness = options[:thickness]
|
30
|
+
self.corners = options[:corners]
|
31
|
+
self.kerf = options[:kerf] || 0
|
32
|
+
self.notch_width = options[:notch_width]
|
33
|
+
self.adjust_corners = options[:adjust_corners]
|
34
|
+
|
35
|
+
adjust_for_kerf!
|
36
|
+
calculate_notch_width!
|
37
|
+
end
|
38
|
+
|
39
|
+
def adjust_for_kerf!
|
40
|
+
if kerf?
|
41
|
+
k = kerf / 2.0
|
42
|
+
p1 = inside.p1.plus(v1 * k)
|
43
|
+
p2 = inside.p2.plus(v2 * k)
|
44
|
+
self.inside = Geometry::Line.new(p1, p2)
|
45
|
+
|
46
|
+
p1 = outside.p1.plus(v1 * k)
|
47
|
+
p2 = outside.p2.plus(v2 * k)
|
48
|
+
self.outside = Geometry::Line.new(p1, p2)
|
49
|
+
|
50
|
+
inside.relocate!
|
51
|
+
outside.relocate!
|
52
|
+
# note – we have not increased the length of the sides to compensate
|
53
|
+
# for kerf – we simply shifted both lines. We'll compenensate for this
|
54
|
+
# in notch width calculations later, and also corner box inclusion.
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def kerf?
|
59
|
+
self.kerf > 0.0
|
60
|
+
end
|
61
|
+
|
62
|
+
# face_setting determines if we want that face to have center notch
|
63
|
+
# facing out (for a hole, etc). This works well when we have odd number
|
64
|
+
# of notches, but
|
65
|
+
def add_across_line?(face_setting)
|
66
|
+
notch_count % 4 == 1 ? face_setting : !face_setting
|
67
|
+
end
|
68
|
+
|
69
|
+
# True if the first notch should be a tab (sticking out), or false if it's a hole.
|
70
|
+
def first_notch_out?
|
71
|
+
add_across_line?(center_out)
|
72
|
+
end
|
73
|
+
private
|
74
|
+
|
75
|
+
def calculate_notch_width!
|
76
|
+
length = kerf? ? self.inside.length - kerf : self.inside.length
|
77
|
+
count = (length / notch_width).to_f.ceil + 1
|
78
|
+
count = (count / 2 * 2) + 1 # make count always an odd number
|
79
|
+
count = [MINIMUM_NOTCHES_PER_SIDE, count].max
|
80
|
+
self.notch_width = 1.0 * length / count
|
81
|
+
self.notch_count = count
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
|
2
|
+
module Laser
|
3
|
+
module Cutter
|
4
|
+
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
|
+
class PathGenerator
|
36
|
+
|
37
|
+
extend Forwardable
|
38
|
+
%i(center_out thickness corners kerf kerf? notch_width first_notch_out? adjust_corners corners).each do |method_name|
|
39
|
+
def_delegator :@edge, method_name, method_name
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_accessor :edge
|
43
|
+
|
44
|
+
# This class generates lines that zigzag between two lines: the outside line, and the
|
45
|
+
# inside line of a single edge. Edge class encapsulates both of them with additional
|
46
|
+
# properties.
|
47
|
+
def initialize(edge)
|
48
|
+
@edge = edge
|
49
|
+
end
|
50
|
+
|
51
|
+
# Calculates a notched path that flows between the outer edge of the box
|
52
|
+
# (outside_line) and inner (inside_line). Relative location of these lines
|
53
|
+
# also defines the direction and orientation of the box, and hence the notches.
|
54
|
+
#
|
55
|
+
# We always want to create a symmetric path that has a notch in the middle
|
56
|
+
# (for center_out = true) or dip in the middle (center_out = false)
|
57
|
+
def generate
|
58
|
+
shifts = define_shifts
|
59
|
+
vertices = []
|
60
|
+
lines = []
|
61
|
+
|
62
|
+
if corners
|
63
|
+
lines << corner_box_sides
|
64
|
+
end
|
65
|
+
|
66
|
+
point = starting_point
|
67
|
+
|
68
|
+
vertices << point
|
69
|
+
adjust_for_kerf(vertices,-1) if adjust_corners && !first_notch_out?
|
70
|
+
shifts.each do |shift|
|
71
|
+
point = shift.next_point_after point
|
72
|
+
vertices << point
|
73
|
+
end
|
74
|
+
adjust_for_kerf(vertices, 1) if adjust_corners && !first_notch_out?
|
75
|
+
lines << create_lines(vertices)
|
76
|
+
lines.flatten
|
77
|
+
end
|
78
|
+
|
79
|
+
def adjust_for_kerf(vertices, direction)
|
80
|
+
if kerf?
|
81
|
+
point = vertices.pop
|
82
|
+
point = corners ? point.plus(2 * direction * shift_vector(1)) : point
|
83
|
+
vertices << point
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def corner_box_sides
|
88
|
+
boxes = []
|
89
|
+
extra_lines = []
|
90
|
+
sides = []
|
91
|
+
|
92
|
+
# These two boxes occupy the corners of the 3D box. They do not match
|
93
|
+
# in width to our notches because they are usually merged with them.
|
94
|
+
# It's just an aesthetic choice I guess.
|
95
|
+
boxes << Geometry::Rect[edge.inside.p1.clone, edge.outside.p1.clone]
|
96
|
+
boxes << Geometry::Rect[edge.inside.p2.clone, edge.outside.p2.clone]
|
97
|
+
|
98
|
+
if kerf?
|
99
|
+
if adjust_corners
|
100
|
+
if first_notch_out?
|
101
|
+
k = 2
|
102
|
+
direction = -1
|
103
|
+
dim_index = 1
|
104
|
+
extra_lines << add_corners_when_out(dim_index, direction, k)
|
105
|
+
else
|
106
|
+
k = -2
|
107
|
+
direction = 1
|
108
|
+
dim_index = 0
|
109
|
+
extra_lines << add_boxes_when_in(dim_index, direction, k)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
sides = boxes.flatten.map(&:relocate!).map(&:sides)
|
114
|
+
sides << extra_lines if !extra_lines.empty?
|
115
|
+
sides.flatten
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_boxes_when_in(dim_index, direction, k)
|
119
|
+
v1 = k * direction * shift_vector(1, dim_index)
|
120
|
+
v2 = k * direction * shift_vector(2, dim_index)
|
121
|
+
p1 = edge.inside.p1.plus(v1)
|
122
|
+
coords = []
|
123
|
+
coords[d_index_along] = edge.inside.p1[d_index_along]
|
124
|
+
coords[d_index_across] = edge.outside.p1[d_index_across]
|
125
|
+
p2 = Geometry::Point[*coords]
|
126
|
+
r1 = Geometry::Rect[p1, p2]
|
127
|
+
|
128
|
+
p1 = edge.inside.p2.plus(v2)
|
129
|
+
coords = []
|
130
|
+
coords[d_index_along] = edge.inside.p2[d_index_along]
|
131
|
+
coords[d_index_across] = edge.outside.p2[d_index_across]
|
132
|
+
p2 = Geometry::Point[*coords]
|
133
|
+
r2 = Geometry::Rect[p1, p2]
|
134
|
+
lines = [r1, r2].map(&:sides).flatten
|
135
|
+
lines << Geometry::Line[edge.inside.p1.plus(v1), edge.inside.p1.clone]
|
136
|
+
lines << Geometry::Line[edge.inside.p2.plus(v2), edge.inside.p2.clone]
|
137
|
+
lines
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_corners_when_out(dim_index, direction, k)
|
141
|
+
v1 = direction * k * shift_vector(1, dim_index)
|
142
|
+
v2 = direction * k * shift_vector(2, dim_index)
|
143
|
+
p1 = edge.inside.p1.plus(v1)
|
144
|
+
coords = []
|
145
|
+
coords[d_index_along] = edge.outside.p1[d_index_along]
|
146
|
+
coords[d_index_across] = edge.inside.p1[d_index_across]
|
147
|
+
p2 = Geometry::Point[*coords]
|
148
|
+
r1 = Geometry::Rect[p1, p2]
|
149
|
+
|
150
|
+
p1 = edge.inside.p2.plus(v2)
|
151
|
+
coords = []
|
152
|
+
coords[d_index_along] = edge.outside.p2[d_index_along]
|
153
|
+
coords[d_index_across] = edge.inside.p2[d_index_across]
|
154
|
+
p2 = Geometry::Point[*coords]
|
155
|
+
r2 = Geometry::Rect[p1, p2]
|
156
|
+
lines = [r1, r2].map(&:sides).flatten
|
157
|
+
lines << Geometry::Line[edge.inside.p1.plus(v1), edge.inside.p1.clone]
|
158
|
+
lines << Geometry::Line[edge.inside.p2.plus(v2), edge.inside.p2.clone]
|
159
|
+
lines
|
160
|
+
end
|
161
|
+
|
162
|
+
def shift_vector(index, dim_shift = 0)
|
163
|
+
shift = []
|
164
|
+
shift[(d_index_across + dim_shift) % 2] = 0
|
165
|
+
shift[(d_index_along + dim_shift) % 2] = kerf / 2.0 * edge.send("v#{index}".to_sym).[]((d_index_along + dim_shift) % 2)
|
166
|
+
Vector.[](*shift)
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
def starting_point
|
171
|
+
edge.inside.p1.clone # start
|
172
|
+
end
|
173
|
+
|
174
|
+
# 0 = X, 1 = Y
|
175
|
+
def d_index_along
|
176
|
+
(edge.inside.p1.x == edge.inside.p2.x) ? 1 : 0
|
177
|
+
end
|
178
|
+
|
179
|
+
def d_index_across
|
180
|
+
(d_index_along + 1) % 2
|
181
|
+
end
|
182
|
+
|
183
|
+
def direction_along
|
184
|
+
(edge.inside.p1.coords.[](d_index_along) < edge.inside.p2.coords.[](d_index_along)) ? 1 : -1
|
185
|
+
end
|
186
|
+
|
187
|
+
def direction_across
|
188
|
+
(edge.inside.p1.coords.[](d_index_across) < edge.outside.p1.coords.[](d_index_across)) ? 1 : -1
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
# This method has the bulk of the logic: we create the list of path deltas
|
194
|
+
# to be applied when we walk the edge next.
|
195
|
+
# @param [Object] shift
|
196
|
+
def define_shifts
|
197
|
+
along_iter = create_iterator_along
|
198
|
+
across_iter = create_iterator_across
|
199
|
+
|
200
|
+
shifts = []
|
201
|
+
inner = true # false when we are drawing outer notch, true when inner
|
202
|
+
|
203
|
+
if first_notch_out?
|
204
|
+
shifts << across_iter.next
|
205
|
+
inner = !inner
|
206
|
+
end
|
207
|
+
|
208
|
+
(1..edge.notch_count).to_a.each do |notch_number|
|
209
|
+
shifts << along_iter.next do |shift, index|
|
210
|
+
if inner && (notch_number > 1 && notch_number < edge.notch_count)
|
211
|
+
shift.delta -= kerf
|
212
|
+
elsif !inner
|
213
|
+
shift.delta += kerf
|
214
|
+
end
|
215
|
+
inner = !inner
|
216
|
+
shift
|
217
|
+
end
|
218
|
+
shifts << across_iter.next unless notch_number == edge.notch_count
|
219
|
+
end
|
220
|
+
|
221
|
+
shifts << across_iter.next if first_notch_out?
|
222
|
+
shifts
|
223
|
+
end
|
224
|
+
|
225
|
+
# As we draw notches, shifts define the 'delta' – movement from one point
|
226
|
+
# to the next. This method defines three types of movements we'll be doing:
|
227
|
+
# one alongside the edge, and two across (towards the box and outward from the box)
|
228
|
+
def create_iterator_along
|
229
|
+
InfiniteIterator.new([Shift.new(notch_width, direction_along, d_index_along)])
|
230
|
+
end
|
231
|
+
|
232
|
+
def create_iterator_across
|
233
|
+
InfiniteIterator.new([Shift.new(thickness, direction_across, d_index_across),
|
234
|
+
Shift.new(thickness, -direction_across, d_index_across)])
|
235
|
+
end
|
236
|
+
|
237
|
+
def create_lines(vertices)
|
238
|
+
lines = []
|
239
|
+
vertices.each_with_index do |v, i|
|
240
|
+
if v != vertices.last
|
241
|
+
lines << Geometry::Line.new(v, vertices[i+1])
|
242
|
+
end
|
243
|
+
end
|
244
|
+
lines.flatten
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -24,8 +24,8 @@ module Laser
|
|
24
24
|
renderer = self
|
25
25
|
pdf.instance_eval do
|
26
26
|
self.line_width = renderer.config.stroke.send(renderer.config.units.to_sym)
|
27
|
-
stroke_color BLACK
|
28
|
-
renderer.box.
|
27
|
+
stroke_color renderer.config[:color] || BLACK
|
28
|
+
renderer.box.generate_notches.each do |notch|
|
29
29
|
LineRenderer.new(renderer.config, notch).render(self)
|
30
30
|
end
|
31
31
|
end
|
@@ -11,23 +11,34 @@ module Laser
|
|
11
11
|
|
12
12
|
def render
|
13
13
|
renderer = self
|
14
|
+
renderers = []
|
14
15
|
|
15
|
-
margin = config.margin.to_f.send(config.units.to_sym)
|
16
|
-
meta_renderer = MetaRenderer.new(config)
|
17
16
|
box_renderer = BoxRenderer.new(config)
|
18
|
-
|
17
|
+
renderers << box_renderer
|
18
|
+
|
19
|
+
if config.metadata
|
20
|
+
meta_renderer = MetaRenderer.new(config)
|
21
|
+
renderers << meta_renderer
|
22
|
+
box_renderer.ensure_space_for(meta_renderer.enclosure)
|
23
|
+
end
|
19
24
|
|
25
|
+
if config.debug
|
26
|
+
unkerfed_config = Laser::Cutter::Configuration.new(config.to_hash)
|
27
|
+
unkerfed_config.merge!(kerf: 0.0, color: 'DD2211')
|
28
|
+
unkerfed_box_renderer = BoxRenderer.new(unkerfed_config)
|
29
|
+
unkerfed_box_renderer.ensure_space_for(meta_renderer.enclosure) if meta_renderer
|
30
|
+
renderers << unkerfed_box_renderer
|
31
|
+
end
|
32
|
+
|
33
|
+
margin = config.margin.to_f.send(config.units.to_sym)
|
20
34
|
page_size = config.page_size || calculate_image_boundary(box_renderer, margin)
|
21
35
|
|
22
36
|
pdf = Prawn::Document.new(:margin => margin,
|
23
37
|
:page_size => page_size,
|
24
|
-
:page_layout => config.page_layout.to_sym)
|
38
|
+
:page_layout => self.config.page_layout.to_sym)
|
25
39
|
|
26
40
|
pdf.instance_eval do
|
27
|
-
|
28
|
-
meta_renderer.render(self)
|
29
|
-
end
|
30
|
-
box_renderer.render(self)
|
41
|
+
renderers.each {|r| r.render(self) }
|
31
42
|
render_file(renderer.config.file)
|
32
43
|
end
|
33
44
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require 'json'
|
3
2
|
class Laser::Cutter::Renderer::MetaRenderer < Laser::Cutter::Renderer::Base
|
4
3
|
|
4
|
+
META_KEYS = %w(units width height depth thickness notch kerf stroke padding margin page_size page_layout)
|
5
|
+
|
5
6
|
def initialize(config = {})
|
6
7
|
self.config = config
|
7
8
|
self.enclosure = Laser::Cutter::Geometry::Rect.create(Laser::Cutter::Geometry::Point[1, 1], 140, 150)
|
@@ -19,11 +20,14 @@ class Laser::Cutter::Renderer::MetaRenderer < Laser::Cutter::Renderer::Base
|
|
19
20
|
EOF
|
20
21
|
|
21
22
|
meta_color = BLUE
|
22
|
-
meta_top_height =
|
23
|
+
meta_top_height = 55
|
23
24
|
|
24
25
|
metadata = config.to_hash
|
25
|
-
metadata.delete_if { |k| %w(verbose metadata open file).include?(k) }
|
26
26
|
metadata['page_size'] ||= 'custom'
|
27
|
+
metadata.delete('page_layout') if metadata['page_size'].eql?('custom')
|
28
|
+
|
29
|
+
meta_fields = META_KEYS.find_all{|k| metadata[k]}.join(": \n") + ": \n"
|
30
|
+
meta_values = META_KEYS.find_all{|k| metadata[k]}.map{|k| metadata[k] }.join("\n")
|
27
31
|
|
28
32
|
rect = self.enclosure
|
29
33
|
|
@@ -41,13 +45,8 @@ class Laser::Cutter::Renderer::MetaRenderer < Laser::Cutter::Renderer::Base
|
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
44
|
-
# print values of the config, in two parts – keys right aligned first,
|
45
|
-
# values left aligned second.
|
48
|
+
# print values of the config, in two parts – keys right aligned first, values left aligned second.
|
46
49
|
float do
|
47
|
-
# parse out meta keys and then values
|
48
|
-
meta_fields = JSON.pretty_generate(metadata).gsub(/[\{\}",]/, '').gsub(/:.*\n/x, "\n")
|
49
|
-
meta_values = JSON.pretty_generate(metadata).gsub(/[\{\}",]/, '').gsub(/\n?.*:/x, "\n:")
|
50
|
-
|
51
50
|
bounding_box([0, rect.h - meta_top_height],
|
52
51
|
:width => rect.w,
|
53
52
|
:height => rect.h - meta_top_height) do
|