laser-cutter 0.5.3 → 1.0.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.
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