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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +20 -16
- data/bin/laser-cutter +4 -158
- data/lib/laser-cutter.rb +2 -0
- data/lib/laser-cutter/aggregator.rb +57 -0
- data/lib/laser-cutter/box.rb +37 -24
- data/lib/laser-cutter/cli/opt_parser.rb +131 -0
- data/lib/laser-cutter/cli/serializer.rb +51 -0
- data/lib/laser-cutter/configuration.rb +3 -2
- data/lib/laser-cutter/geometry.rb +0 -3
- data/lib/laser-cutter/geometry/dimensions.rb +3 -3
- data/lib/laser-cutter/geometry/point.rb +0 -46
- data/lib/laser-cutter/geometry/shape/line.rb +40 -1
- data/lib/laser-cutter/geometry/shape/rect.rb +2 -2
- data/lib/laser-cutter/geometry/tuple.rb +73 -27
- data/lib/laser-cutter/notching.rb +10 -0
- data/lib/laser-cutter/notching/base.rb +13 -0
- data/lib/laser-cutter/notching/edge.rb +87 -0
- data/lib/laser-cutter/notching/path_generator.rb +249 -0
- data/lib/laser-cutter/renderer/base.rb +1 -1
- data/lib/laser-cutter/renderer/box_renderer.rb +2 -2
- data/lib/laser-cutter/renderer/layout_renderer.rb +19 -8
- data/lib/laser-cutter/renderer/meta_renderer.rb +8 -9
- data/lib/laser-cutter/version.rb +1 -1
- data/spec/aggregator_spec.rb +65 -0
- data/spec/box_spec.rb +5 -1
- data/spec/dimensions_spec.rb +0 -1
- data/spec/edge_spec.rb +43 -0
- data/spec/line_spec.rb +42 -19
- data/spec/path_generator_spec.rb +30 -36
- data/spec/point_spec.rb +2 -2
- data/spec/rect_spec.rb +1 -1
- data/spec/renderer_spec.rb +14 -5
- metadata +13 -5
- data/lib/laser-cutter/geometry/edge.rb +0 -33
- data/lib/laser-cutter/geometry/notched_path.rb +0 -46
- 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'
|
@@ -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
|
-
|
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.
|
31
|
+
vertices << p1.plus(w, 0)
|
32
32
|
vertices << p2
|
33
|
-
vertices << p1.
|
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
|
-
|
16
|
+
x.clone
|
17
|
+
elsif x.is_a?(Tuple) or x.is_a?(Vector)
|
18
|
+
x.to_a
|
17
19
|
else
|
18
|
-
|
20
|
+
args.clone
|
19
21
|
end
|
20
|
-
|
21
|
-
self.coords.
|
22
|
+
coordinates.map!(&:to_f)
|
23
|
+
self.coords = Vector.[](*coordinates)
|
22
24
|
end
|
23
25
|
|
24
|
-
def
|
25
|
-
|
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
|
-
|
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.
|
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
|
-
#
|
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
|
-
|
122
|
+
string.split(separator).map(&:to_f)
|
77
123
|
end
|
78
124
|
|
125
|
+
# Return array of coordinates
|
79
126
|
def parse_hash hash
|
80
|
-
|
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
|