laser-cutter 0.2.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.
@@ -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