view graster/graster/lib/graster.rb @ 11:f952052e37b7

trying a fix.
author Robert McIntyre <rlm@mit.edu>
date Tue, 24 Aug 2010 19:06:45 -0400
parents
children
line wrap: on
line source
1 #!/usr/bin/env ruby
3 require 'rubygems'
4 require 'yaml'
5 require 'RMagick'
7 class Graster
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')
14 ROOT2 = Math.sqrt(2)
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 }
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 }
69 class InvalidConfig < Exception; end
70 def update_config
71 @scale = @config[:dpi].map{|n| 1.0/n }
72 @offset = @config[:offset]
74 if @image
75 2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] }
76 @tile_interval = []
77 2.times {|i|
78 @tile_interval << @config[:tile_size][i] + @config[:tile_spacing][i]
79 }
80 @tile_interval
81 end
83 @on_range = Range.new Image.f_to_pix(@config[:on_range].first),
84 Image.f_to_pix(@config[:on_range].last)
85 end
87 def validate_config
88 raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"
89 end
91 def config= h
92 @config = {}
93 DEFAULTS.each {|k,v| @config[k] = h[k] || v }
94 update_config
95 return h
96 end
98 def merge_config h
99 @config ||= DEFAULTS.dup
100 h.each {|k,v| @config[k] = v if DEFAULTS[k] }
101 update_config
102 return h
103 end
105 attr_reader :config
107 def image= img
108 debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels"
109 @image = img
110 @image.build_spans @on_range
111 update_config
112 build_tiled_rows
113 return img
114 end
116 attr_reader :image
118 def try_load_config_file pn
119 if File.exist?(pn)
120 c = {}
121 YAML.load_file(pn).each {|k,v| c[k.intern] = v }
122 return c
123 end
124 end
126 def try_load_default_config_file
127 try_load_config_file './graster.yml'
128 end
130 def load_config_file pn
131 try_load_config_file pn or raise "config file not found '#{pn}'"
132 end
134 def load_image_file pn
135 self.image = Image.from_file(pn)
136 end
138 # convert tile + pixel coordinates to inches
139 def axis_inches axis, tile, pixel
140 @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]
141 end
143 def x_inches tile, pixel
144 axis_inches 0, tile, pixel
145 end
147 def y_inches tile, pixel
148 axis_inches 1, tile, pixel
149 end
151 # return a complete tiled row of spans converted to inches
152 def tiled_row_spans y, forward=true
153 spans = @image.spans[y]
154 return spans if spans.empty?
155 tiled_spans = []
157 if forward
158 @config[:repeat][0].times do |tile|
159 spans.each do |span|
160 tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])]
161 end
162 end
163 else
164 (0...@config[:repeat][0]).to_a.reverse.each do |tile|
165 spans.reverse.each do |span|
166 tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])]
167 end
168 end
169 end
171 return tiled_spans
172 end
174 def build_tiled_rows
175 forward = false
176 @tiled_rows = []
177 @image.size[1].times {|y| @tiled_rows << tiled_row_spans(y, (forward = !forward)) }
178 end
180 # generate a unique id for this job
181 def job_hash
182 [@image,@config].hash
183 end
185 # render a complete tiled image to gcode and gmask streams
186 def render_tiled_image gcode, gmask
187 debug "rendering tiled image"
188 job_id = job_hash
189 hyst = -@scale[0]/2
190 gcode.comment "raster gcode for job #{job_id}"
191 gcode.comment "image: #{@image.filename} #{@image.size.inspect}"
192 gcode.comment "config: #{@config.inspect}"
194 gcode.preamble :feed => @config[:feed], :mask => true
195 gmask.preamble
197 @config[:repeat][1].times do |ytile|
198 debug "begin tile row #{ytile}"
199 ypix = 0
200 (0...@tiled_rows).each do |spans|
201 debug "pixel row #{ypix} is empty" if spans.empty?
202 unless spans.empty?
203 yinches = y_inches(ytile, ypix)
204 forward = spans[0][0] < spans[-1][1]
205 dir = forward ? 1 : -1
207 debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans"
209 gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches
210 gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches
211 gmask.begin_row forward
212 # G0 X0.606 Y1.976
213 # G1 X3.396 Y1.976
214 # G0 X3.392 Y1.978
215 # G1 X0.610 Y1.978
216 # G0 X0.614 Y1.980
217 # G1 X3.388 Y1.980
219 spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }
220 end # unless spans.empty?
221 ypix += 1
222 end # @image.each_row
223 debug "end tile row #{ytile}"
224 end # @config[:repeat][i].times
226 gcode.epilogue
227 end # def render_tiled_image
229 # cut out the tile with bottom left at x,y
230 def render_cut gcode, x, y
231 radius = @config[:corner_radius]
232 left = x
233 bottom = y
234 right = x+@config[:tile_size][0]
235 top = y+@config[:tile_size][1]
237 gcode.instance_eval do
238 if radius && radius > 0
239 jog :x => left, :y => bottom+radius
240 move :x => left, :y => top-radius, :laser => true
241 turn_cw :x => left+radius, :y => top, :i => radius
242 move :x => right-radius, :y => top
243 turn_cw :x => right, :y => top-radius, :j => -radius
244 move :x => right, :y => bottom+radius
245 turn_cw :x => right-radius, :y => bottom, :i => -radius
246 move :x => left+radius, :y => bottom
247 turn_cw :x => left, :y => bottom+radius, :j => radius
248 nc :laser => false
249 else
250 jog :x => left, :y => bottom
251 move :x => left, :y => top, :laser => true
252 move :x => right, :y => top
253 move :x => right, :y => bottom
254 move :x => left, :y => bottom
255 nc :laser => false
256 end
257 end
258 end
260 # render gcode to cut out the tiles
261 def render_all_cuts gcode
262 gcode.preamble :feed => @config[:cut_feed]
263 @config[:repeat][1].times do |ytile|
264 @config[:repeat][0].times do |xtile|
265 render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0)
266 end
267 end
268 gcode.epilogue
269 end
271 def render_all gcode, gmask, cuts
272 render_tiled_image gcode, gmask
273 render_all_cuts cuts
274 end
276 def open_gcode_file &block
277 io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block
278 end
280 def open_gmask_file &block
281 io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block
282 end
284 def open_cut_file &block
285 io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block
286 end
288 def generate_all_files
289 open_gcode_file do |gcode|
290 open_gmask_file do |gmask|
291 render_tiled_image gcode, gmask
292 end
293 end
295 open_cut_file do |cut|
296 render_all_cuts cut
297 end
298 end
300 def config_to_yaml
301 @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join
302 end
304 def debug msg
305 STDERR.puts msg if @debug
306 end
308 def initialize opts={}
309 self.config = DEFAULTS.dup
311 if opts[:config_file]
312 self.merge_config load_config_file opts[:config_file]
313 elsif opts[:default_config_file] && c = try_load_default_config_file
314 self.merge_config c
315 end
317 self.merge_config opts[:config] if opts[:config]
319 @debug = opts[:debug]
321 if opts[:image]
322 image = opts[:image]
323 elsif opts[:image_file]
324 load_image_file opts[:image_file]
325 end
326 end
328 end # class Graster