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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/README.md +20 -16
  4. data/bin/laser-cutter +4 -158
  5. data/lib/laser-cutter.rb +2 -0
  6. data/lib/laser-cutter/aggregator.rb +57 -0
  7. data/lib/laser-cutter/box.rb +37 -24
  8. data/lib/laser-cutter/cli/opt_parser.rb +131 -0
  9. data/lib/laser-cutter/cli/serializer.rb +51 -0
  10. data/lib/laser-cutter/configuration.rb +3 -2
  11. data/lib/laser-cutter/geometry.rb +0 -3
  12. data/lib/laser-cutter/geometry/dimensions.rb +3 -3
  13. data/lib/laser-cutter/geometry/point.rb +0 -46
  14. data/lib/laser-cutter/geometry/shape/line.rb +40 -1
  15. data/lib/laser-cutter/geometry/shape/rect.rb +2 -2
  16. data/lib/laser-cutter/geometry/tuple.rb +73 -27
  17. data/lib/laser-cutter/notching.rb +10 -0
  18. data/lib/laser-cutter/notching/base.rb +13 -0
  19. data/lib/laser-cutter/notching/edge.rb +87 -0
  20. data/lib/laser-cutter/notching/path_generator.rb +249 -0
  21. data/lib/laser-cutter/renderer/base.rb +1 -1
  22. data/lib/laser-cutter/renderer/box_renderer.rb +2 -2
  23. data/lib/laser-cutter/renderer/layout_renderer.rb +19 -8
  24. data/lib/laser-cutter/renderer/meta_renderer.rb +8 -9
  25. data/lib/laser-cutter/version.rb +1 -1
  26. data/spec/aggregator_spec.rb +65 -0
  27. data/spec/box_spec.rb +5 -1
  28. data/spec/dimensions_spec.rb +0 -1
  29. data/spec/edge_spec.rb +43 -0
  30. data/spec/line_spec.rb +42 -19
  31. data/spec/path_generator_spec.rb +30 -36
  32. data/spec/point_spec.rb +2 -2
  33. data/spec/rect_spec.rb +1 -1
  34. data/spec/renderer_spec.rb +14 -5
  35. metadata +13 -5
  36. data/lib/laser-cutter/geometry/edge.rb +0 -33
  37. data/lib/laser-cutter/geometry/notched_path.rb +0 -46
  38. data/lib/laser-cutter/geometry/path_generator.rb +0 -129
@@ -0,0 +1,10 @@
1
+ module Laser
2
+ module Cutter
3
+ module Notching
4
+ end
5
+ end
6
+ end
7
+
8
+ require_relative 'notching/base'
9
+ require_relative 'notching/edge'
10
+ require_relative 'notching/path_generator'
@@ -0,0 +1,13 @@
1
+ module Laser::Cutter::Notching
2
+ class Base
3
+ attr_accessor :edge
4
+
5
+ def initialize(edge)
6
+ @edge = edge
7
+ end
8
+
9
+ def notches
10
+ raise 'Abstract method'
11
+ end
12
+ end
13
+ end
@@ -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
@@ -6,7 +6,7 @@ module Laser
6
6
  # page_manager contains access to units and page sizes
7
7
  class Base
8
8
  BLACK = "000000"
9
- BLUE = "4090E0"
9
+ BLUE = "0070E0"
10
10
 
11
11
  attr_accessor :config, :subject, :enclosure, :page_manager
12
12
 
@@ -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.notches.each do |notch|
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
- box_renderer.ensure_space_for(meta_renderer.enclosure) if config.metadata
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
- if renderer.config.metadata
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 = 50
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