graster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+