graster 0.0.1

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.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "graster"
5
+ s.description = s.summary = "G Raster!"
6
+ s.email = "joshbuddy@gmail.com"
7
+ s.homepage = "http://github.com/joshbuddy/graster"
8
+ s.authors = ["Jedediah Smith", "Joshua Hull"]
9
+ s.files = FileList["[A-Z]*", "{lib,bin}/**/*"]
10
+ s.add_dependency 'RMagick'
11
+ s.rubyforge_project = 'graster'
12
+ end
13
+ Jeweler::GemcutterTasks.new
14
+ Jeweler::RubyforgeTasks.new do |rubyforge|
15
+ rubyforge.doc_task = "rdoc"
16
+ rubyforge.remote_doc_path = ''
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/graster ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+ # Tokyo cache cow command line interface script.
3
+ # Run <tt>tokyo_cache_cow -h</tt> to get more usage.
4
+ require File.dirname(__FILE__) + '/../lib/graster'
5
+
6
+ Graster::Runner.new(ARGV).start!
data/bin/gtile ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+ # Tokyo cache cow command line interface script.
3
+ # Run <tt>tokyo_cache_cow -h</tt> to get more usage.
4
+ require File.dirname(__FILE__) + '/../lib/graster'
5
+
6
+ unless ARGV.size == 5
7
+ puts "usage: ruby tile.rb <input-gcode-file> <tile-width> <tile-height> <horiz-count> <vert-count>"
8
+ exit(1)
9
+ end
10
+
11
+ def parse_line line
12
+ nc = {}
13
+ line.gsub(/\([^)]*\)/,'').upcase.scan(/([A-Z])\s*([0-9\.]+)?/).each {|code| nc[code[0].intern] = (code[1] && code[1].to_f) }
14
+ nc
15
+ end
16
+
17
+ def gcode ncs
18
+ ncs = [ncs] unless ncs.is_a? Array
19
+ ncs.reduce('') {|a,nc| a << (nc.map {|k,v| "#{k}#{v}" }.join(' ') + "\n") }
20
+ end
21
+
22
+ tile_width = ARGV[1].to_f
23
+ tile_height = ARGV[2].to_f
24
+ horiz_count = ARGV[3].to_i
25
+ vert_count = ARGV[4].to_i
26
+
27
+ header = []
28
+ body = []
29
+ footer = []
30
+ state = :header
31
+
32
+
33
+ File.open ARGV[0] do |io|
34
+ io.each_line do |line|
35
+ if (nc = parse_line(line)) != {}
36
+ case state
37
+ when :header
38
+ if nc[:G] == 0 || nc[:G] == 1
39
+ state = :body
40
+ body << nc
41
+ else
42
+ header << nc
43
+ end
44
+
45
+ when :body
46
+ if nc[:G] == 0 || nc[:G] == 1
47
+ body << nc
48
+ else
49
+ state = :footer
50
+ footer << nc
51
+ end
52
+
53
+ when :footer
54
+ footer << nc
55
+ end
56
+ end # case
57
+
58
+ end # io.each_line
59
+ end # File.open
60
+
61
+ print gcode(header)
62
+
63
+ vert_count.times.map do |yc|
64
+ horiz_count.times.map do |xc|
65
+ body.each do |nc|
66
+ nc = nc.dup
67
+ nc[:X] += xc*tile_width if nc[:X]
68
+ nc[:Y] += yc*tile_height if nc[:Y]
69
+ print gcode(nc)
70
+ end
71
+ end
72
+ end
73
+
74
+ print gcode(footer)
data/lib/graster.rb ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'RMagick'
6
+
7
+ class Graster
8
+
9
+ autoload :Runner, File.join(File.dirname(__FILE__), 'graster', 'runner')
10
+ autoload :Image, File.join(File.dirname(__FILE__), 'graster', 'image')
11
+ autoload :GcodeFile, File.join(File.dirname(__FILE__), 'graster', 'gcode_file')
12
+ autoload :GmaskFile, File.join(File.dirname(__FILE__), 'graster', 'gmask_file')
13
+
14
+ ROOT2 = Math.sqrt(2)
15
+
16
+ OPTIONS = {
17
+ :dpi => [[Float],"X,Y","Dots per inch of your device"],
18
+ :on_range => [[Float],
19
+ "MIN,MAX","Luminosity range for which the",
20
+ "laser should be on"],
21
+ :overshoot => [Float,"INCHES",
22
+ "Distance the X axis should travel",
23
+ "past the outer boundaries of the outer",
24
+ "images. This needs to be wide enough",
25
+ "so that the X axis doesn't start",
26
+ "decelerating until after it has",
27
+ "cleared the image"],
28
+ :offset => [[Float],"X,Y",
29
+ "Location for the bottom left corner",
30
+ "of the bottom left tile. The X",
31
+ "component of this setting must be",
32
+ "equal to or greater than overshoot"],
33
+ :repeat => [[Integer],"X,Y",
34
+ "Number of times to repeat the image",
35
+ "in the X and Y axes, respectively.",
36
+ "Size of the tile(s) inches. Any nil",
37
+ "value is calculated from the size of",
38
+ "the bitmap"],
39
+ :tile_spacing => [[Float],"X,Y",
40
+ "X,Y gap between repeated tiles in",
41
+ "inches"],
42
+ :feed => [Float,"N",
43
+ "Speed to move the X axis while",
44
+ "burning, in inches/minute"],
45
+ :cut_feed => [Float,"N",
46
+ "Speed at which to cut out tiles"],
47
+ :corner_radius => [Float,"N",
48
+ "Radius of rounded corners for",
49
+ "cutout, 0 for pointy corners"]
50
+ }
51
+
52
+ DEFAULTS = {
53
+ :dpi => [500,500], # X,Y dots per inch of your device
54
+ :on_range => [0.0,0.5], # Luminosity range for which the laser should be on
55
+ :overshoot => 0.5, # Distance the X axis should travel past the outer boundaries of the outer images.
56
+ # This needs to be wide enough so that the X axis doesn't start decelerating
57
+ # until after it has cleared the image.
58
+ :offset => [1.0,1.0], # X,Y location for the bottom left corner of the bottom left tile.
59
+ # The X component of this setting must be equal to or greater than :overshoot.
60
+ :repeat => [1,1], # Number of times to repeat the image in the X and Y axes, respectively.
61
+ :tile_size => [false,false], # Size of the tile(s) inches. Any nil value is calculated from
62
+ # the size of the bitmap.
63
+ :tile_spacing => [0.125,0.125], # X,Y gap between repeated tiles in inches
64
+ :feed => 120, # Speed to move the X axis while burning, in inches/minute
65
+ :cut_feed => 20, # Speed at which to cut out tiles
66
+ :corner_radius => 0 # Radius of rounded corners for cutout, 0 for pointy corners
67
+ }
68
+
69
+ class InvalidConfig < Exception; end
70
+ def update_config
71
+ @scale = @config[:dpi].map{|n| 1.0/n }
72
+ @offset = @config[:offset]
73
+
74
+ if @image
75
+ 2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] }
76
+ @tile_interval = 2.times.map {|i|
77
+ @config[:tile_size][i] + @config[:tile_spacing][i]
78
+ }
79
+ end
80
+
81
+ @on_range = Range.new Image.f_to_pix(@config[:on_range].first),
82
+ Image.f_to_pix(@config[:on_range].last)
83
+ end
84
+
85
+ def validate_config
86
+ raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"
87
+ end
88
+
89
+ def config= h
90
+ @config = {}
91
+ DEFAULTS.each {|k,v| @config[k] = h[k] || v }
92
+ update_config
93
+ return h
94
+ end
95
+
96
+ def merge_config h
97
+ @config ||= DEFAULTS.dup
98
+ h.each {|k,v| @config[k] = v if DEFAULTS[k] }
99
+ update_config
100
+ return h
101
+ end
102
+
103
+ attr_reader :config
104
+
105
+ def image= img
106
+ debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels"
107
+ @image = img
108
+ @image.build_spans @on_range
109
+ update_config
110
+ build_tiled_rows
111
+ return img
112
+ end
113
+
114
+ attr_reader :image
115
+
116
+ def try_load_config_file pn
117
+ if File.exist?(pn)
118
+ c = {}
119
+ YAML.load_file(pn).each {|k,v| c[k.intern] = v }
120
+ return c
121
+ end
122
+ end
123
+
124
+ def try_load_default_config_file
125
+ try_load_config_file './graster.yml'
126
+ end
127
+
128
+ def load_config_file pn
129
+ try_load_config_file pn or raise "config file not found '#{pn}'"
130
+ end
131
+
132
+ def load_image_file pn
133
+ self.image = Image.from_file(pn)
134
+ end
135
+
136
+ # convert tile + pixel coordinates to inches
137
+ def axis_inches axis, tile, pixel
138
+ @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]
139
+ end
140
+
141
+ def x_inches tile, pixel
142
+ axis_inches 0, tile, pixel
143
+ end
144
+
145
+ def y_inches tile, pixel
146
+ axis_inches 1, tile, pixel
147
+ end
148
+
149
+ # return a complete tiled row of spans converted to inches
150
+ def tiled_row_spans y, forward=true
151
+ spans = @image.spans[y]
152
+ return spans if spans.empty?
153
+ tiled_spans = []
154
+
155
+ if forward
156
+ @config[:repeat][0].times do |tile|
157
+ spans.each do |span|
158
+ tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])]
159
+ end
160
+ end
161
+ else
162
+ @config[:repeat][0].times.reverse_each do |tile|
163
+ spans.reverse_each do |span|
164
+ tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])]
165
+ end
166
+ end
167
+ end
168
+
169
+ return tiled_spans
170
+ end
171
+
172
+ def build_tiled_rows
173
+ forward = false
174
+ @tiled_rows = @image.size[1].times.map {|y| tiled_row_spans y, (forward = !forward) }
175
+ end
176
+
177
+ # generate a unique id for this job
178
+ def job_hash
179
+ [@image,@config].hash
180
+ end
181
+
182
+ # render a complete tiled image to gcode and gmask streams
183
+ def render_tiled_image gcode, gmask
184
+ debug "rendering tiled image"
185
+ job_id = job_hash
186
+ hyst = -@scale[0]/2
187
+ gcode.comment "raster gcode for job #{job_id}"
188
+ gcode.comment "image: #{@image.filename} #{@image.size.inspect}"
189
+ gcode.comment "config: #{@config.inspect}"
190
+
191
+ gcode.preamble :feed => @config[:feed], :mask => true
192
+ gmask.preamble
193
+
194
+ @config[:repeat][1].times do |ytile|
195
+ debug "begin tile row #{ytile}"
196
+ @tiled_rows.each_with_index do |spans, ypix|
197
+ debug "pixel row #{ypix} is empty" if spans.empty?
198
+ unless spans.empty?
199
+ yinches = y_inches(ytile, ypix)
200
+ forward = spans[0][0] < spans[-1][1]
201
+ dir = forward ? 1 : -1
202
+
203
+ debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans"
204
+
205
+ gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches
206
+ gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches
207
+ gmask.begin_row forward
208
+ spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }
209
+ end # unless spans.empty?
210
+ end # @image.each_row
211
+ debug "end tile row #{ytile}"
212
+ end # @config[:repeat][i].times
213
+
214
+ gcode.epilogue
215
+ end # def render_tiled_image
216
+
217
+ # cut out the tile with bottom left at x,y
218
+ def render_cut gcode, x, y
219
+ radius = @config[:corner_radius]
220
+ left = x
221
+ bottom = y
222
+ right = x+@config[:tile_size][0]
223
+ top = y+@config[:tile_size][1]
224
+
225
+ gcode.instance_eval do
226
+ if radius && radius > 0
227
+ jog :x => left, :y => bottom+radius
228
+ move :x => left, :y => top-radius, :laser => true
229
+ turn_cw :x => left+radius, :y => top, :i => radius
230
+ move :x => right-radius, :y => top
231
+ turn_cw :x => right, :y => top-radius, :j => -radius
232
+ move :x => right, :y => bottom+radius
233
+ turn_cw :x => right-radius, :y => bottom, :i => -radius
234
+ move :x => left+radius, :y => bottom
235
+ turn_cw :x => left, :y => bottom+radius, :j => radius
236
+ nc :laser => false
237
+ else
238
+ jog :x => left, :y => bottom
239
+ move :x => left, :y => top, :laser => true
240
+ move :x => right, :y => top
241
+ move :x => right, :y => bottom
242
+ move :x => left, :y => bottom
243
+ nc :laser => false
244
+ end
245
+ end
246
+ end
247
+
248
+ # render gcode to cut out the tiles
249
+ def render_all_cuts gcode
250
+ gcode.preamble :feed => @config[:cut_feed]
251
+ @config[:repeat][1].times do |ytile|
252
+ @config[:repeat][0].times do |xtile|
253
+ render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0)
254
+ end
255
+ end
256
+ gcode.epilogue
257
+ end
258
+
259
+ def render_all gcode, gmask, cuts
260
+ render_tiled_image gcode, gmask
261
+ render_all_cuts cuts
262
+ end
263
+
264
+ def open_gcode_file &block
265
+ io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block
266
+ end
267
+
268
+ def open_gmask_file &block
269
+ io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block
270
+ end
271
+
272
+ def open_cut_file &block
273
+ io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block
274
+ end
275
+
276
+ def generate_all_files
277
+ open_gcode_file do |gcode|
278
+ open_gmask_file do |gmask|
279
+ render_tiled_image gcode, gmask
280
+ end
281
+ end
282
+
283
+ open_cut_file do |cut|
284
+ render_all_cuts cut
285
+ end
286
+ end
287
+
288
+ def config_to_yaml
289
+ @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join
290
+ end
291
+
292
+ def debug msg
293
+ STDERR.puts msg if @debug
294
+ end
295
+
296
+ def initialize opts={}
297
+ self.config = DEFAULTS.dup
298
+
299
+ if opts[:config_file]
300
+ self.merge_config load_config_file opts[:config_file]
301
+ elsif opts[:default_config_file] && c = try_load_default_config_file
302
+ self.merge_config c
303
+ end
304
+
305
+ self.merge_config opts[:config] if opts[:config]
306
+
307
+ @debug = opts[:debug]
308
+
309
+ if opts[:image]
310
+ image = opts[:image]
311
+ elsif opts[:image_file]
312
+ load_image_file opts[:image_file]
313
+ end
314
+ end
315
+
316
+ end # class Graster
@@ -0,0 +1,71 @@
1
+ class Graster
2
+ class GcodeFile < File
3
+ def preamble opts
4
+ @laser = false
5
+ self << "M63 P0\nG61\nF#{opts[:feed] || 60}\n"
6
+ self << "M101\n" if opts[:mask]
7
+ self << "M3 S1\n"
8
+ end
9
+
10
+ def epilogue
11
+ self << "M63 P0\nM5\nM2\n"
12
+ end
13
+
14
+ PRIORITY = [:g,:x,:y,:z,:w,:i,:j,:k,:m,:p,:s]
15
+
16
+ def nc codes
17
+ codes = codes.dup
18
+
19
+ if codes[:laser] == true && !@laser
20
+ @laser = true
21
+ codes.merge!(:m => 62, :p => 0)
22
+ elsif codes[:laser] == false && @laser
23
+ @laser = false
24
+ codes.merge!(:m => 63, :p => 0)
25
+ end
26
+
27
+ codes.delete :laser
28
+
29
+ self << codes.sort {|(k1,v1),(k2,v2)|
30
+ PRIORITY.index(k1) <=> PRIORITY.index(k2)
31
+ }.map {|k,v|
32
+ if v.is_a? Integer
33
+ "#{k.to_s.upcase}#{v}"
34
+ elsif v.is_a? Float
35
+ "#{k.to_s.upcase}%0.3f" % v
36
+ else
37
+ k.to_s.upcase
38
+ end
39
+ }.join(' ') + "\n"
40
+ end
41
+
42
+ def g0 codes
43
+ nc({:g => 0}.merge codes)
44
+ end
45
+ alias_method :jog, :g0
46
+
47
+ def g1 codes
48
+ nc({:g => 1}.merge codes)
49
+ end
50
+ alias_method :move, :g1
51
+
52
+ def g2 codes
53
+ nc codes.merge(:g => 2)
54
+ end
55
+ alias_method :turn_cw, :g2
56
+
57
+ def g3 codes
58
+ nc codes.merge(:g => 3)
59
+ end
60
+ alias_method :turn_ccw, :g3
61
+
62
+ def comment txt
63
+ txt = txt.gsub(/\(\)/,'')
64
+ self << "(#{txt})\n"
65
+ end
66
+
67
+ def puts *a
68
+ self.puts *a
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ class Graster
2
+ class GmaskFile < File
3
+ def preamble
4
+ self << "1 0 0 0\n"
5
+ end
6
+
7
+ def begin_row forward
8
+ @begin_row = true
9
+ end
10
+
11
+ def span forward, x1, x2
12
+ if forward
13
+ self << "0 0 0 %0.3f\n" % x1 if @begin_row
14
+ self << "0 0 1 %0.3f\n" % x1
15
+ self << "0 1 1 %0.3f\n" % x2
16
+ else
17
+ self << "0 0 1 %0.3f\n" % x1 if @begin_row
18
+ self << "0 0 0 %0.3f\n" % x1
19
+ self << "0 1 0 %0.3f\n" % x2
20
+ end
21
+ @begin_row = false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,83 @@
1
+ class Graster
2
+ class Image
3
+ PROPS = [:filename,:size,:pixels]
4
+
5
+ def initialize(props)
6
+ PROPS.each do |p|
7
+ raise "required image property :#{p} missing" unless props[p]
8
+ instance_variable_set "@#{p}", props[p]
9
+ end
10
+ end
11
+
12
+ PROPS.each{|p| attr_reader p }
13
+
14
+ def self.from_file pathname
15
+ raise "file not found #{pathname}" unless File.exist? pathname
16
+ img = Magick::Image.read(pathname)
17
+ raise "bad image data in #{pathname}" unless img = img[0]
18
+ new :filename => File.basename(pathname),
19
+ :size => [img.columns,img.rows],
20
+ :pixels => img.export_pixels(0,0,img.columns,img.rows,"I")
21
+ end
22
+
23
+ # get pixel(s) from x,y coords
24
+ # 0,0 is bottom,left
25
+ # image[x,y] => pixel at x,y
26
+ # image[y] => row at y
27
+ def [] y, x=nil
28
+ if x
29
+ @pixels[(@size[1]-y)*@size[0]+x]
30
+ else
31
+ @pixels[(@size[1]-y)*@size[0],@size[0]]
32
+ end
33
+ end
34
+
35
+ def each_row &block
36
+ @pixels.chars.each_slice(@size[0]).each_with_index &block
37
+ end
38
+
39
+ # "encode" a float 0..1 to a pixel
40
+ def self.f_to_pix f
41
+ (f*65535).round
42
+ end
43
+
44
+ # "decode" an encoded pixel to a float 0..1
45
+ def self.pix_to_f pix
46
+ pix/65535.0
47
+ end
48
+
49
+
50
+ # convert bitmap data to spans (or runs) of contiguous pixels
51
+ # also invert the Y axis
52
+ def build_spans on_range
53
+ # TODO: rewrite in terms of each_row
54
+ @spans = Array.new @size[1]
55
+
56
+ @size[1].times do |y|
57
+ spans = []
58
+ left = (@size[1]-y-1)*@size[0]
59
+ start = nil
60
+
61
+ @size[0].times do |x|
62
+ d = on_range.include?(@pixels[left+x])
63
+
64
+ if !start && d
65
+ start = x
66
+ elsif start && !d
67
+ spans << [start, x]
68
+ start = nil
69
+ end
70
+ end
71
+
72
+ spans << [start, @size[0]] if start
73
+ @spans[y] = spans
74
+ end
75
+ end
76
+
77
+ attr_reader :spans
78
+
79
+ def hash
80
+ [@pixels,@width,@height].hash
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,66 @@
1
+ require 'optparse'
2
+
3
+ class Graster
4
+ class Runner
5
+
6
+ attr_reader :options, :args, :opts
7
+
8
+ def initialize(args)
9
+ @args = args
10
+ @options = { :default_config_file => true }
11
+ @opts = OptionParser.new do |opts|
12
+ opts.banner = "Usage: graster [options] image"
13
+
14
+ opts.on "-c", "--config FILE", "Use specified configuration file.",
15
+ "The default is ./graster.yml" do |c|
16
+ @options[:config_file] = c
17
+ end
18
+
19
+ opts.on "-g", "--generate", "generate a configuration file with","defaults" do
20
+ @options[:generate_config] = true
21
+ end
22
+
23
+ opts.on "-d", "--debug", "Dump useless debug info" do
24
+ @options[:debug] = true
25
+ end
26
+
27
+ Graster::OPTIONS.each do |key,info|
28
+ type,sym,*desc = info
29
+
30
+ if type.is_a? Array
31
+ cast = type[0].name.intern
32
+ type = Array
33
+ else
34
+ cast = type.name.intern
35
+ end
36
+
37
+ opts.on "--#{key.to_s.gsub /_/, '-'} #{sym}", type, *desc do |x|
38
+ @options[:config] ||= {}
39
+ if type == Array
40
+ x = x.map {|s| Kernel.send(cast,s) }
41
+ else
42
+ x = Kernel.send(cast,x)
43
+ end
44
+
45
+ @options[:config][key] = x
46
+ end
47
+ end
48
+ end
49
+
50
+ @opts.parse!(args)
51
+ end
52
+
53
+ def start!
54
+ if @options[:generate_config]
55
+ print Graster.new(@options).config_to_yaml
56
+ else
57
+ unless options[:image_file] = args.shift
58
+ puts @opts
59
+ exit 1
60
+ end
61
+
62
+ Graster.new(options).generate_all_files
63
+ end
64
+ end
65
+ end
66
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jedediah Smith
8
+ - Joshua Hull
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-10-20 00:00:00 -04:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: RMagick
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
26
+ description: G Raster!
27
+ email: joshbuddy@gmail.com
28
+ executables:
29
+ - graster
30
+ - gtile
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - Rakefile
37
+ - VERSION
38
+ - bin/graster
39
+ - bin/gtile
40
+ - lib/graster.rb
41
+ - lib/graster/gcode_file.rb
42
+ - lib/graster/gmask_file.rb
43
+ - lib/graster/image.rb
44
+ - lib/graster/runner.rb
45
+ has_rdoc: true
46
+ homepage: http://github.com/joshbuddy/graster
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options:
51
+ - --charset=UTF-8
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project: graster
69
+ rubygems_version: 1.3.5
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: G Raster!
73
+ test_files: []
74
+