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.
- 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
|