laser-cutter 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +67 -0
- data/Rakefile +2 -0
- data/bin/laser-cutter +110 -0
- data/laser-cutter.gemspec +29 -0
- data/lib/laser-cutter.rb +12 -0
- data/lib/laser-cutter/box.rb +136 -0
- data/lib/laser-cutter/configuration.rb +56 -0
- data/lib/laser-cutter/geometry.rb +14 -0
- data/lib/laser-cutter/geometry/dimensions.rb +30 -0
- data/lib/laser-cutter/geometry/edge.rb +33 -0
- data/lib/laser-cutter/geometry/notched_path.rb +46 -0
- data/lib/laser-cutter/geometry/path_generator.rb +132 -0
- data/lib/laser-cutter/geometry/point.rb +57 -0
- data/lib/laser-cutter/geometry/shape.rb +36 -0
- data/lib/laser-cutter/geometry/shape/line.rb +66 -0
- data/lib/laser-cutter/geometry/shape/rect.rb +59 -0
- data/lib/laser-cutter/geometry/tuple.rb +89 -0
- data/lib/laser-cutter/renderer.rb +19 -0
- data/lib/laser-cutter/renderer/box_renderer.rb +69 -0
- data/lib/laser-cutter/renderer/line_renderer.rb +16 -0
- data/lib/laser-cutter/renderer/rect_renderer.rb +17 -0
- data/lib/laser-cutter/version.rb +5 -0
- data/spec/box_spec.rb +31 -0
- data/spec/configuration_spec.rb +28 -0
- data/spec/dimensions_spec.rb +28 -0
- data/spec/line_spec.rb +79 -0
- data/spec/path_generator_spec.rb +94 -0
- data/spec/point_spec.rb +74 -0
- data/spec/rect_spec.rb +53 -0
- data/spec/renderer_spec.rb +60 -0
- data/spec/spec_helper.rb +26 -0
- metadata +177 -0
@@ -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
|