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,59 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Geometry
|
4
|
+
class Rect < Line
|
5
|
+
|
6
|
+
attr_accessor :sides, :vertices
|
7
|
+
|
8
|
+
def self.[] p1, p2
|
9
|
+
Rect.new(p1, p2)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.create(point, w, h, name = nil)
|
13
|
+
r = Rect.new(point, Point[point.x + w, point.y + h])
|
14
|
+
r.name = name
|
15
|
+
r
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_line(line)
|
19
|
+
Rect.new(line.p1, line.p2)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(*args)
|
23
|
+
super(*args)
|
24
|
+
relocate!
|
25
|
+
end
|
26
|
+
|
27
|
+
def relocate!
|
28
|
+
super
|
29
|
+
self.vertices = []
|
30
|
+
vertices << p1
|
31
|
+
vertices << p1.move_by(w, 0)
|
32
|
+
vertices << p2
|
33
|
+
vertices << p1.move_by(0, h)
|
34
|
+
self.sides = []
|
35
|
+
vertices.each_with_index do |v, index|
|
36
|
+
sides << Line.new(v, vertices[(index + 1) % vertices.size])
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def w
|
42
|
+
p2.x - p1.x
|
43
|
+
end
|
44
|
+
|
45
|
+
def h
|
46
|
+
p2.y - p1.y
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
"#{sprintf "%3d", w}(w)x#{sprintf "%3d", h}(h) @ #{position.to_s}"
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Geometry
|
4
|
+
class Tuple
|
5
|
+
attr_accessor :coords
|
6
|
+
PRECISION = 0.000001
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
args = customize_args(args)
|
10
|
+
x = args.first
|
11
|
+
if x.is_a?(String)
|
12
|
+
parse_string(x)
|
13
|
+
elsif x.is_a?(Hash)
|
14
|
+
parse_hash(x)
|
15
|
+
elsif x.is_a?(Array)
|
16
|
+
self.coords = x.clone
|
17
|
+
else
|
18
|
+
self.coords = args.clone
|
19
|
+
end
|
20
|
+
|
21
|
+
self.coords.map!(&:to_f)
|
22
|
+
end
|
23
|
+
|
24
|
+
def customize_args(args)
|
25
|
+
args
|
26
|
+
end
|
27
|
+
|
28
|
+
def separator
|
29
|
+
raise NotImplementedError
|
30
|
+
# 'x'
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash_keys
|
34
|
+
raise NotImplementedError
|
35
|
+
# [:x, :y, :z] or [:h, :w, :d]
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_a
|
39
|
+
self.coords
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"{#{coords.map { |a| sprintf("%.5f", a) }.join(separator)}}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid?
|
47
|
+
raise "Have nil value: #{self.inspect}" if coords.any? { |c| c.nil? }
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def eql?(other)
|
52
|
+
return false unless other.respond_to?(:coords)
|
53
|
+
equal = true
|
54
|
+
self.coords.each_with_index do |c, i|
|
55
|
+
if (c - other.coords[i])**2 > PRECISION
|
56
|
+
equal = false
|
57
|
+
break
|
58
|
+
end
|
59
|
+
end
|
60
|
+
equal
|
61
|
+
end
|
62
|
+
|
63
|
+
def clone
|
64
|
+
clone = super
|
65
|
+
clone.coords = self.coords.clone
|
66
|
+
clone
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
#
|
72
|
+
# convert from, eg "100,50" to [100.0, 50.0],
|
73
|
+
# and then to a new instance.
|
74
|
+
#
|
75
|
+
def parse_string string
|
76
|
+
self.coords = string.split(separator).map(&:to_f)
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_hash hash
|
80
|
+
self.coords = []
|
81
|
+
hash_keys.each { |k| self.coords << hash[k] }
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Renderer
|
4
|
+
class AbstractRenderer < Struct.new(:subject, :options)
|
5
|
+
def render pdf = nil
|
6
|
+
raise 'Abstract method'
|
7
|
+
end
|
8
|
+
|
9
|
+
def units
|
10
|
+
options.units.to_sym || :mm
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'laser-cutter/renderer/line_renderer'
|
18
|
+
require 'laser-cutter/renderer/rect_renderer'
|
19
|
+
require 'laser-cutter/renderer/box_renderer'
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'json'
|
2
|
+
module Laser
|
3
|
+
module Cutter
|
4
|
+
module Renderer
|
5
|
+
class BoxRenderer < AbstractRenderer
|
6
|
+
|
7
|
+
def box
|
8
|
+
subject
|
9
|
+
end
|
10
|
+
|
11
|
+
def render pdf = nil
|
12
|
+
pdf = Prawn::Document.new(:margin => options.margin.send(options.units),
|
13
|
+
:page_size => options.page_size,
|
14
|
+
:page_layout => options.page_layout.to_sym)
|
15
|
+
|
16
|
+
header = <<-EOF
|
17
|
+
|
18
|
+
Produced with Laser Cutter Ruby Gem (v#{Laser::Cutter::VERSION})
|
19
|
+
Credits to Prawn (for ruby PDF generation),
|
20
|
+
and BoxMaker (for the inspiration).
|
21
|
+
© 2014 Konstantin Gredeskoul, MIT license.
|
22
|
+
https://github.com/kigster/laser-cutter
|
23
|
+
Generated at #{Time.new}.
|
24
|
+
EOF
|
25
|
+
|
26
|
+
renderer = self
|
27
|
+
|
28
|
+
pdf.instance_eval do
|
29
|
+
self.line_width = renderer.options.stroke.send(renderer.options.units.to_sym)
|
30
|
+
float do
|
31
|
+
bounding_box([0, 50], :width => 150, :height => 40) do
|
32
|
+
stroke_color '0080FF'
|
33
|
+
stroke_bounds
|
34
|
+
|
35
|
+
indent 10 do
|
36
|
+
font('Courier', :size => 5) do
|
37
|
+
text header, :color => "0080FF"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
bounding_box([480, 120], :width => 110, :height => 110) do
|
42
|
+
stroke_color '00DF20'
|
43
|
+
stroke_bounds
|
44
|
+
indent 10 do
|
45
|
+
font('Courier', :size => 6) do
|
46
|
+
out = JSON.pretty_generate(renderer.options.to_hash).gsub(/[\{\}",]/,'')
|
47
|
+
text out, :color => "00DF20"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
stroke_color "000000"
|
54
|
+
renderer.box.notches.each do |notch|
|
55
|
+
LineRenderer.new(notch, renderer.options).render(self)
|
56
|
+
end
|
57
|
+
|
58
|
+
render_file(renderer.options.file)
|
59
|
+
end
|
60
|
+
|
61
|
+
if options.verbose
|
62
|
+
puts "file #{options.file} has been written."
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Renderer
|
4
|
+
class LineRenderer < AbstractRenderer
|
5
|
+
def line
|
6
|
+
subject
|
7
|
+
end
|
8
|
+
def render pdf = nil
|
9
|
+
pdf.stroke { pdf.line [line.p1.x, line.p1.y].map{ |p| p.send(units) },
|
10
|
+
[line.p2.x, line.p2.y].map{ |p| p.send(units) }}
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Laser
|
2
|
+
module Cutter
|
3
|
+
module Renderer
|
4
|
+
class RectRenderer < AbstractRenderer
|
5
|
+
def rect
|
6
|
+
subject
|
7
|
+
end
|
8
|
+
|
9
|
+
def render pdf
|
10
|
+
rect.sides.each do |side|
|
11
|
+
LineRenderer.new(side, options).render(pdf)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/box_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module Laser
|
4
|
+
module Cutter
|
5
|
+
describe Box do
|
6
|
+
let(:config) { {'width' => 50, 'height' => 60, 'depth' => 70, 'margin' => 5, 'padding' => 3 } }
|
7
|
+
let(:box1) { Box.new(config.merge('thickness' => 6, 'notch' => 10)) }
|
8
|
+
let(:box2) { Box.new(config.merge('thickness' => 6, )) }
|
9
|
+
|
10
|
+
context '#initialize' do
|
11
|
+
it 'should initialize with passed in parameters' do
|
12
|
+
expect(box1.w).to eq(50.0)
|
13
|
+
expect(box1.thickness).to eq(6.0)
|
14
|
+
expect(box1.notch_width).to eq(10.0)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should initialize with default notch' do
|
18
|
+
expect(box2.notch_width).to eq(70.0 / 5.0)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context '#notches' do
|
23
|
+
it 'should generate notches' do
|
24
|
+
expect(box1.notches).to_not be_nil
|
25
|
+
expect(box1.notches.size).to eql(320)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module Laser
|
4
|
+
module Cutter
|
5
|
+
describe Configuration do
|
6
|
+
let(:config) { Laser::Cutter::Configuration.new(opts)}
|
7
|
+
|
8
|
+
context 'option parsing' do
|
9
|
+
let(:opts) { { "size" => "2x3x2/0.125/0.5", "inches" => true} }
|
10
|
+
it 'should be able to parse size options' do
|
11
|
+
expect(config.width).to eql(2.0)
|
12
|
+
expect(config.height).to eql(3.0)
|
13
|
+
expect(config.depth).to eql(2.0)
|
14
|
+
expect(config.thickness).to eql(0.125)
|
15
|
+
expect(config.notch).to eql(0.5)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context 'validate' do
|
19
|
+
let(:opts) {{ "height" => "23" }}
|
20
|
+
it 'should be able to validate missing options' do
|
21
|
+
expect(config.height).to eql(23.0)
|
22
|
+
expect { config.validate! } .to raise_error(RuntimeError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module Laser
|
4
|
+
module Cutter
|
5
|
+
module Geometry
|
6
|
+
|
7
|
+
describe Dimensions do
|
8
|
+
let(:dim1) { Dimensions.new(20, 10, 50) }
|
9
|
+
|
10
|
+
context 'creation' do
|
11
|
+
context 'from string' do
|
12
|
+
let(:dim2) { Dimensions.new "20x10x50" }
|
13
|
+
it 'should instantiate correctly from a string' do
|
14
|
+
expect(dim2).to_not be_nil
|
15
|
+
expect(dim2).to eql(dim1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'from hash' do
|
20
|
+
it 'should instantiate correctly from a hash' do
|
21
|
+
expect(Dimensions.new(h: 10, w: 20, d: 50)).to eql(dim1)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/line_spec.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module Laser
|
4
|
+
module Cutter
|
5
|
+
module Geometry
|
6
|
+
describe Line do
|
7
|
+
let(:p1) { Point.new(1, 1) }
|
8
|
+
let(:p2) { Point.new(7, 11) }
|
9
|
+
let(:center) { Point.new( (7 + 1) / 2, (11 + 1) / 2) }
|
10
|
+
let(:line) { Line.new(p1, p2) }
|
11
|
+
|
12
|
+
context '#center' do
|
13
|
+
it 'should calculate' do
|
14
|
+
expect(line.center).to eql(center)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context '#initialize' do
|
19
|
+
let(:line2) { Line.new(from: [1,1], to: [7,11])}
|
20
|
+
let(:line3) { Line.new(from: Point.new(1,1), to: Point.new(7,11))}
|
21
|
+
it 'should create' do
|
22
|
+
expect(line2.p1).to eql(Point.new(1,1))
|
23
|
+
expect(line2.p2).to eql(Point.new(7,11))
|
24
|
+
end
|
25
|
+
it 'should properly equal identical line' do
|
26
|
+
expect(line).to eql(line2)
|
27
|
+
expect(line).to eql(line3)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
context '#length' do
|
33
|
+
let(:line1) { Line.new(Point.new(0, 0), Point.new(0, 10)) }
|
34
|
+
let(:line2) { Line.new(Point.new(0, 0), Point.new(-10, 0)) }
|
35
|
+
let(:line3) { Line.new(Point.new(0, 0), Point.new(3, 4)) }
|
36
|
+
it 'should calculate' do
|
37
|
+
expect(line1.length).to be_within(0.001).of(10)
|
38
|
+
expect(line2.length).to be_within(0.001).of(10)
|
39
|
+
expect(line3.length).to be_within(0.001).of(5)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'ordering and equality' do
|
44
|
+
let(:l1) { Line.new(Point[0,0], Point[10,10]) }
|
45
|
+
let(:l2) { Line.new(Point[0,1], Point[10,10]) }
|
46
|
+
let(:l3) { Line.new(Point[0,0], Point[11,10]) }
|
47
|
+
let(:l4) { Line.new(Point[20,20], Point[1,1]) }
|
48
|
+
let(:l5) { Line.new(Point[11,10], Point[0,0]) }
|
49
|
+
it 'should detect equality' do
|
50
|
+
expect(l1).to eql(Line.new(l1.p1, l1.p2))
|
51
|
+
expect(l1).to_not eql(Line.new(l1.p1, Point[2,4]))
|
52
|
+
expect(l5).to eql(l3)
|
53
|
+
expect(l5.hash).to eql(l3.hash)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should properly compare' do
|
57
|
+
list = [l4,l3,l1,l2]
|
58
|
+
list.sort!
|
59
|
+
expect(list).to eql([l1,l3,l2,l4])
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should properly uniq' do
|
63
|
+
list = [l4, l1, l4, l2, l3, l3, l2, l1]
|
64
|
+
list.sort!.uniq!
|
65
|
+
expect(list).to eql([l1,l3,l2,l4])
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should properly deduplicate' do
|
69
|
+
list = [l1, l4, l2, l3, l5, l3, l1, l3, l3, l2]
|
70
|
+
new_list = PathGenerator.deduplicate(list)
|
71
|
+
expect(new_list).to eql([l4])
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|