laser-cutter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ require 'hashie/mash'
2
+ require 'prawn/measurement_extensions'
3
+
4
+
5
+ module Laser
6
+ module Cutter
7
+ class Configuration < Hashie::Mash
8
+ DEFAULTS = {
9
+ units: 'mm',
10
+ page_size: 'LETTER',
11
+ page_layout: 'portrait'
12
+ }
13
+
14
+ UNIT_SPECIFIC_DEFAULTS = {
15
+ 'mm' => {
16
+ margin: 5,
17
+ padding: 5,
18
+ stroke: 0.0254,
19
+ },
20
+ 'in' => {
21
+ margin: 0.125,
22
+ padding: 0.1,
23
+ stroke: 0.001,
24
+ }
25
+ }
26
+
27
+ SIZE_REGEXP = /[\d\.]+x[\d\.]+x[\d\.]+\/[\d\.]+\/[\d\.]+/
28
+
29
+ FLOATS = %w(width height depth thickness notch margin padding stroke)
30
+ REQUIRED = %w(width height depth thickness notch file)
31
+
32
+ def initialize(options = {})
33
+ options.delete_if{|k,v| v.nil?}
34
+ self.merge!(DEFAULTS)
35
+ self.merge!(options)
36
+ if self['size'] && self['size'] =~ SIZE_REGEXP
37
+ dim, self['thickness'], self['notch'] = self['size'].split('/')
38
+ self['width'],self['height'],self['depth'] = dim.split('x')
39
+ delete('size')
40
+ end
41
+ FLOATS.each do |k|
42
+ self[k] = self[k].to_f if (self[k] && self[k].is_a?(String))
43
+ end
44
+ self.merge!(UNIT_SPECIFIC_DEFAULTS[self['units']].merge(self))
45
+ end
46
+
47
+ def validate!
48
+ missing = []
49
+ REQUIRED.each do |k|
50
+ missing << k if self[k].nil?
51
+ end
52
+ raise "#{missing.join(', ')} #{missing.size > 1 ? 'are' : 'is'} required, but missing." unless missing.empty?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ MINIMUM_NOTCHES_PER_SIDE = 3
5
+ end
6
+ end
7
+ end
8
+
9
+ require 'laser-cutter/geometry/tuple'
10
+ require 'laser-cutter/geometry/dimensions'
11
+ require 'laser-cutter/geometry/point'
12
+ require 'laser-cutter/geometry/shape'
13
+ require 'laser-cutter/geometry/edge'
14
+ require 'laser-cutter/geometry/path_generator'
@@ -0,0 +1,30 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ class Dimensions < Tuple
5
+
6
+ def w
7
+ coords[0]
8
+ end
9
+
10
+ def h
11
+ coords[1]
12
+ end
13
+
14
+ def d
15
+ coords[2]
16
+ end
17
+
18
+ def separator
19
+ 'x'
20
+ end
21
+
22
+ def hash_keys
23
+ [:w, :h, :d]
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,33 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ # This class represents a single edge of one side: both inside
5
+ # and outside edge of the material. It's also responsible
6
+ # for calculating the "perfect" notch width.
7
+ class Edge < Struct.new(:outside, :inside, :notch_width)
8
+ attr_accessor :notch_count
9
+
10
+ def initialize(*args)
11
+ super(*args)
12
+ adjust_notch(self.inside)
13
+ end
14
+
15
+ def adjust_notch(line)
16
+ d = (line.length / notch_width).to_f.ceil
17
+ pairs = d / 2
18
+ d = pairs * 2 + 1
19
+ d = MINIMUM_NOTCHES_PER_SIDE if d < MINIMUM_NOTCHES_PER_SIDE
20
+ self.notch_width = line.length / (1.0 * d)
21
+ self.notch_count = d
22
+ end
23
+
24
+ # face_setting determines if we want that face to have center notch
25
+ # facing out (for a hole, etc). This works well when we have odd number
26
+ # of notches, but
27
+ def add_across_line?(face_setting)
28
+ notch_count % 4 == 1 ? face_setting : !face_setting
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ class NotchedPath
5
+ attr_accessor :lines, :vertices, :corner_boxes
6
+ def initialize(vertices = [])
7
+ @vertices = vertices
8
+ @lines = []
9
+ @corner_boxes = []
10
+ end
11
+
12
+ def << value
13
+ self.vertices << value
14
+ end
15
+
16
+ def [] value
17
+ self.vertices[value]
18
+ end
19
+
20
+ def size
21
+ self.vertices.size
22
+ end
23
+
24
+ def create_lines
25
+ self.lines = []
26
+ self.vertices.each_with_index do |v, i|
27
+ if v != vertices.last
28
+ self.lines << Line.new(v, vertices[i+1])
29
+ end
30
+ end
31
+ self.corner_boxes.each do |box|
32
+ box.relocate!
33
+ self.lines << box.sides
34
+ end
35
+
36
+ self.lines.flatten!
37
+ self.lines
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
46
+
@@ -0,0 +1,132 @@
1
+ require_relative 'notched_path'
2
+
3
+ module Laser
4
+ module Cutter
5
+ module Geometry
6
+ class Shift < Struct.new(:delta, :direction, :dim_index)
7
+ POINTERS = {[1, 0] => ' ->',
8
+ [-1, 0] => '<- ',
9
+ [1, 1] => ' V ',
10
+ [-1, 1] => ' ^ '}
11
+
12
+ def next(point1)
13
+ p = Point.new(point1.to_a)
14
+ p.coords[dim_index] += (delta * direction)
15
+ p
16
+ end
17
+ def to_s
18
+ "shift by:#{sprintf('%.2f', delta)}, #{POINTERS[[direction,dim_index]]}"
19
+ end
20
+ end
21
+
22
+ # Alternating iterator
23
+ class InfiniteIterator < Struct.new(:shift_array)
24
+ attr_accessor :current_index
25
+ def next
26
+ self.current_index = -1 if current_index.nil?
27
+ self.current_index += 1
28
+ self.current_index = current_index % shift_array.size
29
+ shift_array[current_index]
30
+ end
31
+ end
32
+
33
+ class PathGenerator
34
+ # Removes the items from the list that appear more than once
35
+ # Unlike uniq-ing which keeps all elements, just ensures that are not
36
+ # repeated, here we remove elements completely if they are seen more than once.
37
+ # This is used to remove lines
38
+ def self.deduplicate list
39
+ new_list = []
40
+ list.sort!
41
+ list.each_with_index do |e, i|
42
+ next if i < (list.size - 1) && e.eql?(list[i + 1])
43
+ next if i > 0 && e.eql?(list[i - 1])
44
+ new_list << e
45
+ end
46
+ new_list
47
+ end
48
+
49
+ attr_accessor :notch_width, :thickness
50
+ attr_accessor :center_out, :fill_corners
51
+
52
+ def initialize(options = {})
53
+ @notch_width = options[:notch_width] # only desired, will be adapted for each line
54
+ @center_out = options[:center_out] # when true, the notch in the middle of the edge is out, not in.
55
+ @thickness = options[:thickness]
56
+
57
+ # 2D array of booleans. If true first or second end of the edge is
58
+ # created with a corner filled in.
59
+ @fill_corners = options[:fill_corners]
60
+ end
61
+
62
+ # Calculates a notched path that flows between the outer edge of the box
63
+ # (outside_line) and inner (inside_line). Relative location of these lines
64
+ # also defines the direction and orientation of the box, and hence the notches.
65
+ #
66
+ # We always want to create a symmetric path that has a notch in the middle
67
+ # (for center_out = true) or dip in the middle (center_out = false)
68
+ def path(edge)
69
+ shifts = define_shifts(edge)
70
+
71
+ path = NotchedPath.new
72
+
73
+ if fill_corners
74
+ r1 = Geometry::Rect.new(edge.inside.p1, edge.outside.p1)
75
+ r2 = Geometry::Rect.new(edge.inside.p2, edge.outside.p2)
76
+ path.corner_boxes << r1
77
+ path.corner_boxes << r2
78
+ end
79
+
80
+ point = edge.inside.p1.clone
81
+ vertices = [point]
82
+ shifts.each do |shift|
83
+ point = shift.next(point)
84
+ vertices << point
85
+ end
86
+ path.vertices = vertices
87
+ path
88
+ end
89
+
90
+ private
91
+
92
+ # This method has the bulk of the logic: we create the list of path deltas
93
+ # to be applied when we walk the edge next.
94
+ def define_shifts(edge)
95
+ along_iterator, across_iterator = define_shift_iterators(edge)
96
+ shifts = []
97
+
98
+ shifts << across_iterator.next if edge.add_across_line?(center_out)
99
+
100
+ (1..edge.notch_count).to_a.each do |count|
101
+ shifts << along_iterator.next
102
+ shifts << across_iterator.next unless count == edge.notch_count
103
+ end
104
+
105
+ shifts << across_iterator.next if edge.add_across_line?(center_out)
106
+ shifts
107
+ end
108
+
109
+ # As we draw notches, shifts define the 'delta' – movement from one point
110
+ # to the next. This method defines three types of movements we'll be doing:
111
+ # one alongside the edge, and two across (towards the box and outward from the box)
112
+ def define_shift_iterators(edge)
113
+ alongside_dimension = (edge.inside.p1.x == edge.inside.p2.x) ? 1 : 0
114
+ alongside_direction = (edge.inside.p1.coords[alongside_dimension] <
115
+ edge.inside.p2.coords[alongside_dimension]) ? 1 : -1
116
+
117
+ across_dimension = (alongside_dimension + 1) % 2
118
+ across_direction = (edge.inside.p1.coords[across_dimension] >
119
+ edge.outside.p1.coords[across_dimension]) ? -1 : 1
120
+
121
+ [
122
+ InfiniteIterator.new(
123
+ [Shift.new(edge.notch_width, alongside_direction, alongside_dimension)]),
124
+ InfiniteIterator.new(
125
+ [Shift.new(thickness, across_direction, across_dimension),
126
+ Shift.new(thickness, -across_direction, across_dimension)])
127
+ ]
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,57 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ class Point < Tuple
5
+ def self.[] *array
6
+ Point.new *array
7
+ end
8
+
9
+ def customize_args(args)
10
+ if args.first.is_a?(Point)
11
+ return args.first.to_a
12
+ end
13
+ args
14
+ end
15
+
16
+ def x= value
17
+ coords[0] = value
18
+ end
19
+
20
+ def x
21
+ coords[0]
22
+ end
23
+
24
+ def y= value
25
+ coords[1] = value
26
+ end
27
+
28
+ def y
29
+ coords[1]
30
+ end
31
+
32
+ def separator
33
+ ','
34
+ end
35
+
36
+ def hash_keys
37
+ [:x, :y]
38
+ end
39
+
40
+ def move_by w, h
41
+ Point.new(x + w, y + h)
42
+ end
43
+
44
+ def <=>(other)
45
+ self.x == other.x ? self.y <=> other.y : self.x <=> other.x
46
+ end
47
+
48
+ def < (other)
49
+ self.x == other.x ? self.y < other.y : self.x < other.x
50
+ end
51
+ def > (other)
52
+ self.x == other.x ? self.y > other.y : self.x > other.x
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ class Shape
5
+ attr_accessor :position, :name
6
+
7
+ def position
8
+ @position ||= Point.new(0, 0)
9
+ end
10
+
11
+ def x= value
12
+ position.x = value
13
+ end
14
+
15
+ def y= value
16
+ position.y = value
17
+ end
18
+
19
+ def move_to new_point
20
+ self.position = new_point
21
+ relocate!
22
+ self
23
+ end
24
+
25
+
26
+ # Implement in each shape to move to the new pointd
27
+ def relocate!
28
+ raise 'Abstract method'
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ require_relative 'shape/line'
36
+ require_relative 'shape/rect'
@@ -0,0 +1,66 @@
1
+ module Laser
2
+ module Cutter
3
+ module Geometry
4
+ class Line < Shape
5
+ attr_accessor :p1, :p2
6
+
7
+ def initialize(point1, point2 = nil)
8
+ if point1.is_a?(Hash)
9
+ options = point1
10
+ self.p1 = Point.new(options[:from])
11
+ self.p2 = Point.new(options[:to])
12
+ else
13
+ self.p1 = point1.clone
14
+ self.p2 = point2.clone
15
+ end
16
+ self.position = p1.clone
17
+ raise 'Both points are required for line definition' unless (p1 && p2)
18
+ end
19
+
20
+ def relocate!
21
+ dx = p2.x - p1.x
22
+ dy = p2.y - p1.y
23
+
24
+ self.p1 = position.clone
25
+ self.p2 = Point[p1.x + dx, p1.y + dy]
26
+ self
27
+ end
28
+
29
+ def center
30
+ Point.new((p2.x + p1.x) / 2, (p2.y + p1.y) / 2)
31
+ end
32
+
33
+ def length
34
+ Math.sqrt((p2.x - p1.x)**2 + (p2.y - p1.y)**2)
35
+ end
36
+
37
+ def to_s
38
+ "#{self.class.name.gsub(/.*::/,'').downcase} #{p1}=>#{p2}"
39
+ end
40
+
41
+ def eql?(other)
42
+ (other.p1.eql?(p1) && other.p2.eql?(p2)) ||
43
+ (other.p2.eql?(p1) && other.p1.eql?(p2))
44
+ end
45
+
46
+ def normalized
47
+ p1 < p2 ? Line.new(p1, p2) : Line.new(p2, p1)
48
+ end
49
+
50
+ def <=>(other)
51
+ self.normalized.to_s <=> other.normalized.to_s
52
+ end
53
+
54
+ def hash
55
+ [p1.to_a, p2.to_a].sort.hash
56
+ end
57
+
58
+ def clone
59
+ self.class.new(p1, p2)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
66
+ end