laser-cutter 0.5.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/README.md +20 -16
  4. data/bin/laser-cutter +4 -158
  5. data/lib/laser-cutter.rb +2 -0
  6. data/lib/laser-cutter/aggregator.rb +57 -0
  7. data/lib/laser-cutter/box.rb +37 -24
  8. data/lib/laser-cutter/cli/opt_parser.rb +131 -0
  9. data/lib/laser-cutter/cli/serializer.rb +51 -0
  10. data/lib/laser-cutter/configuration.rb +3 -2
  11. data/lib/laser-cutter/geometry.rb +0 -3
  12. data/lib/laser-cutter/geometry/dimensions.rb +3 -3
  13. data/lib/laser-cutter/geometry/point.rb +0 -46
  14. data/lib/laser-cutter/geometry/shape/line.rb +40 -1
  15. data/lib/laser-cutter/geometry/shape/rect.rb +2 -2
  16. data/lib/laser-cutter/geometry/tuple.rb +73 -27
  17. data/lib/laser-cutter/notching.rb +10 -0
  18. data/lib/laser-cutter/notching/base.rb +13 -0
  19. data/lib/laser-cutter/notching/edge.rb +87 -0
  20. data/lib/laser-cutter/notching/path_generator.rb +249 -0
  21. data/lib/laser-cutter/renderer/base.rb +1 -1
  22. data/lib/laser-cutter/renderer/box_renderer.rb +2 -2
  23. data/lib/laser-cutter/renderer/layout_renderer.rb +19 -8
  24. data/lib/laser-cutter/renderer/meta_renderer.rb +8 -9
  25. data/lib/laser-cutter/version.rb +1 -1
  26. data/spec/aggregator_spec.rb +65 -0
  27. data/spec/box_spec.rb +5 -1
  28. data/spec/dimensions_spec.rb +0 -1
  29. data/spec/edge_spec.rb +43 -0
  30. data/spec/line_spec.rb +42 -19
  31. data/spec/path_generator_spec.rb +30 -36
  32. data/spec/point_spec.rb +2 -2
  33. data/spec/rect_spec.rb +1 -1
  34. data/spec/renderer_spec.rb +14 -5
  35. metadata +13 -5
  36. data/lib/laser-cutter/geometry/edge.rb +0 -33
  37. data/lib/laser-cutter/geometry/notched_path.rb +0 -46
  38. data/lib/laser-cutter/geometry/path_generator.rb +0 -129
@@ -0,0 +1,131 @@
1
+ require 'optparse'
2
+ require 'colored'
3
+ require 'json'
4
+ require 'hashie/mash'
5
+ require 'laser-cutter'
6
+ require_relative 'serializer'
7
+
8
+ module Laser
9
+ module Cutter
10
+ module CLI
11
+ class OptParser
12
+ def self.puts_error(e)
13
+ STDERR.puts "Whoops, #{e}".red
14
+ STDERR.puts "Try --help or --examples for more info...".yellow
15
+ end
16
+
17
+ def self.parse(args)
18
+ banner_text = <<-EOF
19
+ #{('Laser-Cutter v'+ Laser::Cutter::VERSION).bold}
20
+
21
+ Usage: laser-cutter [options] -o filename.pdf
22
+ eg: laser-cutter -z 1x1.5x2/0.125 -O -o box.pdf
23
+ EOF
24
+
25
+ examples = <<-EOF
26
+
27
+ Examples:
28
+ 1. Create a box defined in inches, set kerf to 0.008" and open PDF in preview right after:
29
+
30
+ laser-cutter -z 3x2x2/0.125 -k 0.008 -O -o box.pdf
31
+
32
+ 2. Create a box defined in millimeters, print verbose info, and set
33
+ page size to A3, and layout to landscape, and stroke width to 1/2mm:
34
+
35
+ laser-cutter -u mm -w70 -h20 -d50 -t4.3 -n5 -iA3 -l landscape -s0.5 -v -O -o box.pdf
36
+
37
+ 3. List all possible page sizes in metric systems:
38
+
39
+ laser-cutter -L -u mm
40
+
41
+ 4. Create a box with provided dimensions, and save the config to a file
42
+ for later use:
43
+
44
+ laser-cutter -z 1.1x2.5x1.5/0.125/0.125 -p 0.1 -O -o box.pdf -W box-settings.json
45
+
46
+ 5. Read settings from a previously saved file:
47
+
48
+ laser-cutter -O -o box.pdf -R box-settings.json
49
+ cat box-settings.json | laser-cutter -O -o box.pdf -R -
50
+
51
+ EOF
52
+ options = Hashie::Mash.new
53
+ options.verbose = false
54
+ options.units = 'in'
55
+
56
+ opt_parser = OptionParser.new do |opts|
57
+ opts.banner = banner_text.blue
58
+ opts.separator "Specific Options:"
59
+ opts.on("-w", "--width WIDTH", "Internal width of the box") { |value| options.width = value }
60
+ opts.on("-h", "--height HEIGHT", "Internal height of the box") { |value| options.height = value }
61
+ opts.on("-d", "--depth DEPTH", "Internal depth of the box") { |value| options.depth= value }
62
+ opts.on("-t", "--thickness THICKNESS", "Thickness of the box material") { |value| options.thickness = value }
63
+ opts.on("-n", "--notch NOTCH", "Optional notch length (aka \"tab width\"), guide only") { |value| options.notch = value }
64
+ opts.on("-k", "--kerf KERF", "Kerf - cut width (default is 0.007in)") { |value| options.kerf = value }
65
+ opts.separator ""
66
+ opts.on("-m", "--margin MARGIN", "Margins from the edge of the document") { |value| options.margin = value }
67
+ opts.on("-p", "--padding PADDING", "Space between the boxes on the page") { |value| options.padding = value }
68
+ opts.on("-s", "--stroke WIDTH", "Numeric stroke width of the line") { |value| options.stroke = value }
69
+ opts.on("-i", "--page_size LETTER", "Document page size, default is autofit the box.") { |value| options.page_size = value }
70
+ opts.on("-l", "--page_layout portrait", "Page layout, other option is 'landscape' ") { |value| options.page_layout = value }
71
+ opts.separator ""
72
+ opts.on("-O", "--open", "Open generated file with system viewer before exiting") { |v| options.open = v }
73
+ opts.on("-W", "--write CONFIG_FILE", "Save provided configuration to a file, use '-' for STDOUT") { |v| options.write_file = v }
74
+ opts.on("-R", "--read CONFIG_FILE", "Read configuration from a file, or use '-' for STDIN") { |v| options.read_file = v }
75
+ opts.separator ""
76
+ opts.on("-L", "--list-all-page-sizes", "Print all available page sizes with dimensions and exit") { |v| options.list_all_page_sizes = true }
77
+ opts.on("-M", "--no-metadata", "Do not print box metadata on the PDF") { |value| options.metadata = value }
78
+ opts.on("-v", "--[no-]verbose", "Run verbosely") { |v| options.verbose = v }
79
+ opts.on("-B", "--inside-box", "Draw the inside boxes (helpful to verify kerfing)") { |v| options.inside_box = v }
80
+ opts.on("-D", "--debug", "Show full exception stack trace on error") { |v| options.debug = v }
81
+ opts.separator ""
82
+ opts.on("--examples", "Show detailed usage examples") { puts opts; puts examples.yellow; exit }
83
+ opts.on("--help", "Show this message") { puts opts; exit }
84
+ opts.on("--version", "Show version") { puts Laser::Cutter::VERSION; exit }
85
+ opts.separator ""
86
+ opts.separator "Common Options:"
87
+ opts.on_tail("-o", "--file FILE", "Required output filename of the PDF") { |value| options.file = value }
88
+ opts.on_tail("-z", "--size WxHxD/T[/N]",
89
+ "Combined internal dimensions: W = width, H = height,\n#{" " * 37}D = depth, T = thickness, and optional N = notch length\n\n") do |size|
90
+ options.size = size
91
+ end
92
+ opts.on_tail("-u", "--units UNITS", "Either 'in' for inches (default) or 'mm'") { |value| options.units = value }
93
+ end
94
+
95
+ opt_parser.parse!(args)
96
+
97
+ if options.read_file
98
+ # these options are kept from the command line
99
+ override_with = %w(debug verbose read_file)
100
+ keep = options.reject{ |k,v| !override_with.include?(k)}
101
+ Serializer.new(options).deserialize
102
+ options.merge!(keep)
103
+ end
104
+
105
+ config = Laser::Cutter::Configuration.new(options.to_hash)
106
+ if config.list_all_page_sizes
107
+ puts PageManager.new(config.units).all_page_sizes
108
+ exit 0
109
+ end
110
+
111
+ if options.verbose
112
+ puts "Starting with the following configuration:"
113
+ puts JSON.pretty_generate(config.to_hash).green
114
+ end
115
+
116
+ config.validate!
117
+
118
+ if config.write_file
119
+ Serializer.new(config).serialize
120
+ end
121
+
122
+ config
123
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument, Laser::Cutter::MissingOption => e
124
+ puts opt_parser.banner.blue
125
+ puts_error(e)
126
+ exit 1
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,51 @@
1
+ require 'json'
2
+ require 'laser-cutter'
3
+
4
+ module Laser
5
+ module Cutter
6
+ module CLI
7
+ class Serializer
8
+ attr_accessor :options
9
+ def initialize(options = {})
10
+ self.options = options
11
+ end
12
+
13
+ def deserialize
14
+ string = if options.read_file.eql?('-')
15
+ $stdin.read
16
+ elsif File.exist?(options.read_file)
17
+ File.read(options.read_file)
18
+ end
19
+ if string
20
+ options.replace(JSON.load(string))
21
+ end
22
+ rescue Exception => e
23
+ STDERR.puts "Error reading options from file #{options.read_file}, #{e.message}".red
24
+ if options.verbose
25
+ STDERR.puts e.backtrace.join("\n").red
26
+ end
27
+ exit 1
28
+ end
29
+
30
+ def serialize
31
+ output = if options.write_file.eql?('-')
32
+ $stdout
33
+ elsif options.write_file
34
+ File.open(options.write_file, 'w')
35
+ else
36
+ nil
37
+ end
38
+ output.puts(JSON.pretty_generate(options))
39
+ output.close if output != $stdout
40
+ rescue Exception => e
41
+ STDERR.puts "Error writing options to file #{options.write_file}, #{e.message}".red
42
+ if options.verbose
43
+ STDERR.puts e.backtrace.join("\n").red
44
+ end
45
+ exit 1
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
@@ -13,6 +13,7 @@ module Laser
13
13
  units: 'in',
14
14
  page_layout: 'portrait',
15
15
  metadata: true
16
+ #kerf: 0.007 # smallest kerf for thin material, usually it's more than that.
16
17
  }
17
18
 
18
19
  UNIT_SPECIFIC_DEFAULTS = {
@@ -30,7 +31,7 @@ module Laser
30
31
 
31
32
  SIZE_REGEXP = /[\d\.]+x[\d\.]+x[\d\.]+\/[\d\.]+(\/[\d\.]+)?/
32
33
 
33
- FLOATS = %w(width height depth thickness notch margin padding stroke)
34
+ FLOATS = %w(width height depth thickness notch margin padding stroke kerf)
34
35
  NON_ZERO = %w(width height depth thickness stroke)
35
36
  REQUIRED = %w(width height depth thickness notch file)
36
37
 
@@ -50,7 +51,7 @@ module Laser
50
51
  self[k] = self[k].to_f if (self[k] && self[k].is_a?(String))
51
52
  end
52
53
  self.merge!(UNIT_SPECIFIC_DEFAULTS[self['units']].merge(self))
53
- self['notch'] = self['thickness'] * 3.0 if self['thickness'] && self['notch'].nil?
54
+ self['notch'] = (self['thickness'] * 3.0).round(5) if self['thickness'] && self['notch'].nil?
54
55
  end
55
56
 
56
57
  def validate!
@@ -1,7 +1,6 @@
1
1
  module Laser
2
2
  module Cutter
3
3
  module Geometry
4
- MINIMUM_NOTCHES_PER_SIDE = 3
5
4
  end
6
5
  end
7
6
  end
@@ -10,5 +9,3 @@ require 'laser-cutter/geometry/tuple'
10
9
  require 'laser-cutter/geometry/dimensions'
11
10
  require 'laser-cutter/geometry/point'
12
11
  require 'laser-cutter/geometry/shape'
13
- require 'laser-cutter/geometry/edge'
14
- require 'laser-cutter/geometry/path_generator'
@@ -4,15 +4,15 @@ module Laser
4
4
  class Dimensions < Tuple
5
5
 
6
6
  def w
7
- coords[0]
7
+ coords.[](0)
8
8
  end
9
9
 
10
10
  def h
11
- coords[1]
11
+ coords.[](1)
12
12
  end
13
13
 
14
14
  def d
15
- coords[2]
15
+ coords.[](2)
16
16
  end
17
17
 
18
18
  def separator
@@ -5,52 +5,6 @@ module Laser
5
5
  def self.[] *array
6
6
  Point.new *array
7
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
8
  end
55
9
  end
56
10
  end
@@ -2,6 +2,11 @@ module Laser
2
2
  module Cutter
3
3
  module Geometry
4
4
  class Line < Shape
5
+
6
+ def self.[] *array
7
+ self.new *array
8
+ end
9
+
5
10
  attr_accessor :p1, :p2
6
11
 
7
12
  def initialize(point1, point2 = nil)
@@ -17,6 +22,30 @@ module Laser
17
22
  raise 'Both points are required for line definition' unless (p1 && p2)
18
23
  end
19
24
 
25
+ def overlaps?(another)
26
+ xs, ys = sorted_coords(another)
27
+ return false unless xs.all?{|x| x == xs[0] } || ys.all?{|y| y == ys[0] }
28
+ return false if xs.first[1] < xs.last[0] || xs.first[0] > xs.last[1]
29
+ return false if ys.first[1] < ys.last[0] || ys.first[0] > ys.last[1]
30
+ true
31
+ end
32
+
33
+ def sorted_coords(another)
34
+ xs = [[p1.x, p2.x].sort, [another.p1.x, another.p2.x].sort]
35
+ ys = [[p1.y, p2.y].sort, [another.p1.y, another.p2.y].sort]
36
+ return xs, ys
37
+ end
38
+
39
+ def xor(another)
40
+ return nil unless overlaps?(another)
41
+ xs, ys = sorted_coords(another)
42
+ xs.flatten!.sort!
43
+ ys.flatten!.sort!
44
+
45
+ [ Line.new(Point[xs[0], ys[0]], Point[xs[1], ys[1]]),
46
+ Line.new(Point[xs[2], ys[2]], Point[xs[3], ys[3]])]
47
+ end
48
+
20
49
  def relocate!
21
50
  dx = p2.x - p1.x
22
51
  dy = p2.y - p1.y
@@ -48,7 +77,17 @@ module Laser
48
77
  end
49
78
 
50
79
  def <=>(other)
51
- self.normalized.to_s <=> other.normalized.to_s
80
+ n1 = self.normalized
81
+ n2 = other.normalized
82
+ n1.p1.eql?(n2.p1) ? n1.p2 <=> n2.p2 : n1.p1 <=> n2.p1
83
+ end
84
+
85
+ def < (other)
86
+ self.p1 == other.p1 ? self.p2 < other.p2 : self.p1 < other.p1
87
+ end
88
+
89
+ def > (other)
90
+ self.p1 == other.p1 ? self.p2 > other.p2 : self.p1 > other.p1
52
91
  end
53
92
 
54
93
  def hash
@@ -28,9 +28,9 @@ module Laser
28
28
  super
29
29
  self.vertices = []
30
30
  vertices << p1
31
- vertices << p1.move_by(w, 0)
31
+ vertices << p1.plus(w, 0)
32
32
  vertices << p2
33
- vertices << p1.move_by(0, h)
33
+ vertices << p1.plus(0, h)
34
34
  self.sides = []
35
35
  vertices.each_with_index do |v, index|
36
36
  sides << Line.new(v, vertices[(index + 1) % vertices.size])
@@ -1,3 +1,4 @@
1
+ require 'matrix'
1
2
  module Laser
2
3
  module Cutter
3
4
  module Geometry
@@ -6,60 +7,107 @@ module Laser
6
7
  PRECISION = 0.000001
7
8
 
8
9
  def initialize(*args)
9
- args = customize_args(args)
10
10
  x = args.first
11
- if x.is_a?(String)
11
+ coordinates = if x.is_a?(String)
12
12
  parse_string(x)
13
13
  elsif x.is_a?(Hash)
14
14
  parse_hash(x)
15
15
  elsif x.is_a?(Array)
16
- self.coords = x.clone
16
+ x.clone
17
+ elsif x.is_a?(Tuple) or x.is_a?(Vector)
18
+ x.to_a
17
19
  else
18
- self.coords = args.clone
20
+ args.clone
19
21
  end
20
-
21
- self.coords.map!(&:to_f)
22
+ coordinates.map!(&:to_f)
23
+ self.coords = Vector.[](*coordinates)
22
24
  end
23
25
 
24
- def customize_args(args)
25
- args
26
+ def + x, y = nil
27
+ shift = if x.is_a?(Vector)
28
+ x
29
+ elsif x.is_a?(Tuple)
30
+ x.coords
31
+ elsif y
32
+ Vector.[](x,y)
33
+ end
34
+ self.class.new(self.coords + shift)
26
35
  end
27
36
 
28
- def separator
29
- raise NotImplementedError
30
- # 'x'
31
- end
32
37
 
33
- def hash_keys
34
- raise NotImplementedError
35
- # [:x, :y, :z] or [:h, :w, :d]
36
- end
38
+ alias_method :plus, :+
37
39
 
38
40
  def to_a
39
- self.coords.clone
41
+ self.coords.to_a
40
42
  end
41
43
 
42
44
  def to_s
43
- "{#{coords.map { |a| sprintf("%.5f", a) }.join(separator)}}"
45
+ "{#{coords.to_a.map { |a| sprintf("%.5f", a) }.join(separator)}}"
44
46
  end
45
47
 
46
48
  def valid?
47
- raise "Have nil value: #{self.inspect}" if coords.any? { |c| c.nil? }
49
+ raise "Have nil value: #{self.inspect}" if coords.to_a.any? { |c| c.nil? }
48
50
  true
49
51
  end
50
52
 
53
+ def x= value
54
+ self.coords = Vector.[](value, coords.[](1))
55
+ end
56
+
57
+ def y= value
58
+ self.coords = Vector.[](coords.[](0), value)
59
+ end
60
+
61
+ def x
62
+ coords.[](0)
63
+ end
64
+
65
+ def y
66
+ coords.[](1)
67
+ end
68
+
69
+ def separator
70
+ ','
71
+ end
72
+
73
+ def [] value
74
+ coords.[](value)
75
+ end
76
+
77
+
78
+ # Override in subclasses, eg:
79
+ # def separator
80
+ # ';'
81
+ # end
82
+ #
83
+ # def hash_keys
84
+ # [:x, :y, :z] or [:h, :w, :d]
85
+ # end
86
+ def hash_keys
87
+ [:x, :y]
88
+ end
89
+
90
+ # Identity, cloning and sorting/ordering
51
91
  def eql?(other)
52
92
  return false unless other.respond_to?(:coords)
53
93
  equal = true
54
94
  self.coords.each_with_index do |c, i|
55
- if (c - other.coords[i])**2 > PRECISION
95
+ if (c - other.coords.to_a[i])**2 > PRECISION
56
96
  equal = false
57
97
  break
58
98
  end
59
99
  end
60
100
  equal
61
101
  end
62
-
102
+ def <=>(other)
103
+ self.x == other.x ? self.y <=> other.y : self.x <=> other.x
104
+ end
105
+ def < (other)
106
+ self.x == other.x ? self.y < other.y : self.x < other.x
107
+ end
108
+ def > (other)
109
+ self.x == other.x ? self.y > other.y : self.x > other.x
110
+ end
63
111
  def clone
64
112
  clone = super
65
113
  clone.coords = self.coords.clone
@@ -69,16 +117,14 @@ module Laser
69
117
  private
70
118
 
71
119
  #
72
- # convert from, eg "100,50" to [100.0, 50.0],
73
- # and then to a new instance.
74
- #
120
+ # Convert from, eg "100,50" to [100.0, 50.0],
75
121
  def parse_string string
76
- self.coords = string.split(separator).map(&:to_f)
122
+ string.split(separator).map(&:to_f)
77
123
  end
78
124
 
125
+ # Return array of coordinates
79
126
  def parse_hash hash
80
- self.coords = []
81
- hash_keys.each { |k| self.coords << hash[k] }
127
+ hash_keys.map{ |k,v| hash[k] }
82
128
  end
83
129
 
84
130
  end