annotate 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
rev   line source
rlm@11 1 #!/usr/bin/env ruby
rlm@11 2
rlm@11 3 require 'rubygems'
rlm@11 4 require 'yaml'
rlm@11 5 require 'RMagick'
rlm@11 6
rlm@11 7 class Graster
rlm@11 8
rlm@11 9 autoload :Runner, File.join(File.dirname(__FILE__), 'graster', 'runner')
rlm@11 10 autoload :Image, File.join(File.dirname(__FILE__), 'graster', 'image')
rlm@11 11 autoload :GcodeFile, File.join(File.dirname(__FILE__), 'graster', 'gcode_file')
rlm@11 12 autoload :GmaskFile, File.join(File.dirname(__FILE__), 'graster', 'gmask_file')
rlm@11 13
rlm@11 14 ROOT2 = Math.sqrt(2)
rlm@11 15
rlm@11 16 OPTIONS = {
rlm@11 17 :dpi => [[Float],"X,Y","Dots per inch of your device"],
rlm@11 18 :on_range => [[Float],
rlm@11 19 "MIN,MAX","Luminosity range for which the",
rlm@11 20 "laser should be on"],
rlm@11 21 :overshoot => [Float,"INCHES",
rlm@11 22 "Distance the X axis should travel",
rlm@11 23 "past the outer boundaries of the outer",
rlm@11 24 "images. This needs to be wide enough",
rlm@11 25 "so that the X axis doesn't start",
rlm@11 26 "decelerating until after it has",
rlm@11 27 "cleared the image"],
rlm@11 28 :offset => [[Float],"X,Y",
rlm@11 29 "Location for the bottom left corner",
rlm@11 30 "of the bottom left tile. The X",
rlm@11 31 "component of this setting must be",
rlm@11 32 "equal to or greater than overshoot"],
rlm@11 33 :repeat => [[Integer],"X,Y",
rlm@11 34 "Number of times to repeat the image",
rlm@11 35 "in the X and Y axes, respectively.",
rlm@11 36 "Size of the tile(s) inches. Any nil",
rlm@11 37 "value is calculated from the size of",
rlm@11 38 "the bitmap"],
rlm@11 39 :tile_spacing => [[Float],"X,Y",
rlm@11 40 "X,Y gap between repeated tiles in",
rlm@11 41 "inches"],
rlm@11 42 :feed => [Float,"N",
rlm@11 43 "Speed to move the X axis while",
rlm@11 44 "burning, in inches/minute"],
rlm@11 45 :cut_feed => [Float,"N",
rlm@11 46 "Speed at which to cut out tiles"],
rlm@11 47 :corner_radius => [Float,"N",
rlm@11 48 "Radius of rounded corners for",
rlm@11 49 "cutout, 0 for pointy corners"]
rlm@11 50 }
rlm@11 51
rlm@11 52 DEFAULTS = {
rlm@11 53 :dpi => [500,500], # X,Y dots per inch of your device
rlm@11 54 :on_range => [0.0,0.5], # Luminosity range for which the laser should be on
rlm@11 55 :overshoot => 0.5, # Distance the X axis should travel past the outer boundaries of the outer images.
rlm@11 56 # This needs to be wide enough so that the X axis doesn't start decelerating
rlm@11 57 # until after it has cleared the image.
rlm@11 58 :offset => [1.0,1.0], # X,Y location for the bottom left corner of the bottom left tile.
rlm@11 59 # The X component of this setting must be equal to or greater than :overshoot.
rlm@11 60 :repeat => [1,1], # Number of times to repeat the image in the X and Y axes, respectively.
rlm@11 61 :tile_size => [false,false], # Size of the tile(s) inches. Any nil value is calculated from
rlm@11 62 # the size of the bitmap.
rlm@11 63 :tile_spacing => [0.125,0.125], # X,Y gap between repeated tiles in inches
rlm@11 64 :feed => 120, # Speed to move the X axis while burning, in inches/minute
rlm@11 65 :cut_feed => 20, # Speed at which to cut out tiles
rlm@11 66 :corner_radius => 0 # Radius of rounded corners for cutout, 0 for pointy corners
rlm@11 67 }
rlm@11 68
rlm@11 69 class InvalidConfig < Exception; end
rlm@11 70 def update_config
rlm@11 71 @scale = @config[:dpi].map{|n| 1.0/n }
rlm@11 72 @offset = @config[:offset]
rlm@11 73
rlm@11 74 if @image
rlm@11 75 2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] }
rlm@11 76 @tile_interval = []
rlm@11 77 2.times {|i|
rlm@11 78 @tile_interval << @config[:tile_size][i] + @config[:tile_spacing][i]
rlm@11 79 }
rlm@11 80 @tile_interval
rlm@11 81 end
rlm@11 82
rlm@11 83 @on_range = Range.new Image.f_to_pix(@config[:on_range].first),
rlm@11 84 Image.f_to_pix(@config[:on_range].last)
rlm@11 85 end
rlm@11 86
rlm@11 87 def validate_config
rlm@11 88 raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"
rlm@11 89 end
rlm@11 90
rlm@11 91 def config= h
rlm@11 92 @config = {}
rlm@11 93 DEFAULTS.each {|k,v| @config[k] = h[k] || v }
rlm@11 94 update_config
rlm@11 95 return h
rlm@11 96 end
rlm@11 97
rlm@11 98 def merge_config h
rlm@11 99 @config ||= DEFAULTS.dup
rlm@11 100 h.each {|k,v| @config[k] = v if DEFAULTS[k] }
rlm@11 101 update_config
rlm@11 102 return h
rlm@11 103 end
rlm@11 104
rlm@11 105 attr_reader :config
rlm@11 106
rlm@11 107 def image= img
rlm@11 108 debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels"
rlm@11 109 @image = img
rlm@11 110 @image.build_spans @on_range
rlm@11 111 update_config
rlm@11 112 build_tiled_rows
rlm@11 113 return img
rlm@11 114 end
rlm@11 115
rlm@11 116 attr_reader :image
rlm@11 117
rlm@11 118 def try_load_config_file pn
rlm@11 119 if File.exist?(pn)
rlm@11 120 c = {}
rlm@11 121 YAML.load_file(pn).each {|k,v| c[k.intern] = v }
rlm@11 122 return c
rlm@11 123 end
rlm@11 124 end
rlm@11 125
rlm@11 126 def try_load_default_config_file
rlm@11 127 try_load_config_file './graster.yml'
rlm@11 128 end
rlm@11 129
rlm@11 130 def load_config_file pn
rlm@11 131 try_load_config_file pn or raise "config file not found '#{pn}'"
rlm@11 132 end
rlm@11 133
rlm@11 134 def load_image_file pn
rlm@11 135 self.image = Image.from_file(pn)
rlm@11 136 end
rlm@11 137
rlm@11 138 # convert tile + pixel coordinates to inches
rlm@11 139 def axis_inches axis, tile, pixel
rlm@11 140 @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]
rlm@11 141 end
rlm@11 142
rlm@11 143 def x_inches tile, pixel
rlm@11 144 axis_inches 0, tile, pixel
rlm@11 145 end
rlm@11 146
rlm@11 147 def y_inches tile, pixel
rlm@11 148 axis_inches 1, tile, pixel
rlm@11 149 end
rlm@11 150
rlm@11 151 # return a complete tiled row of spans converted to inches
rlm@11 152 def tiled_row_spans y, forward=true
rlm@11 153 spans = @image.spans[y]
rlm@11 154 return spans if spans.empty?
rlm@11 155 tiled_spans = []
rlm@11 156
rlm@11 157 if forward
rlm@11 158 @config[:repeat][0].times do |tile|
rlm@11 159 spans.each do |span|
rlm@11 160 tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])]
rlm@11 161 end
rlm@11 162 end
rlm@11 163 else
rlm@11 164 (0...@config[:repeat][0]).to_a.reverse.each do |tile|
rlm@11 165 spans.reverse.each do |span|
rlm@11 166 tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])]
rlm@11 167 end
rlm@11 168 end
rlm@11 169 end
rlm@11 170
rlm@11 171 return tiled_spans
rlm@11 172 end
rlm@11 173
rlm@11 174 def build_tiled_rows
rlm@11 175 forward = false
rlm@11 176 @tiled_rows = []
rlm@11 177 @image.size[1].times {|y| @tiled_rows << tiled_row_spans(y, (forward = !forward)) }
rlm@11 178 end
rlm@11 179
rlm@11 180 # generate a unique id for this job
rlm@11 181 def job_hash
rlm@11 182 [@image,@config].hash
rlm@11 183 end
rlm@11 184
rlm@11 185 # render a complete tiled image to gcode and gmask streams
rlm@11 186 def render_tiled_image gcode, gmask
rlm@11 187 debug "rendering tiled image"
rlm@11 188 job_id = job_hash
rlm@11 189 hyst = -@scale[0]/2
rlm@11 190 gcode.comment "raster gcode for job #{job_id}"
rlm@11 191 gcode.comment "image: #{@image.filename} #{@image.size.inspect}"
rlm@11 192 gcode.comment "config: #{@config.inspect}"
rlm@11 193
rlm@11 194 gcode.preamble :feed => @config[:feed], :mask => true
rlm@11 195 gmask.preamble
rlm@11 196
rlm@11 197 @config[:repeat][1].times do |ytile|
rlm@11 198 debug "begin tile row #{ytile}"
rlm@11 199 ypix = 0
rlm@11 200 (0...@tiled_rows).each do |spans|
rlm@11 201 debug "pixel row #{ypix} is empty" if spans.empty?
rlm@11 202 unless spans.empty?
rlm@11 203 yinches = y_inches(ytile, ypix)
rlm@11 204 forward = spans[0][0] < spans[-1][1]
rlm@11 205 dir = forward ? 1 : -1
rlm@11 206
rlm@11 207 debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans"
rlm@11 208
rlm@11 209 gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches
rlm@11 210 gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches
rlm@11 211 gmask.begin_row forward
rlm@11 212 # G0 X0.606 Y1.976
rlm@11 213 # G1 X3.396 Y1.976
rlm@11 214 # G0 X3.392 Y1.978
rlm@11 215 # G1 X0.610 Y1.978
rlm@11 216 # G0 X0.614 Y1.980
rlm@11 217 # G1 X3.388 Y1.980
rlm@11 218
rlm@11 219 spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }
rlm@11 220 end # unless spans.empty?
rlm@11 221 ypix += 1
rlm@11 222 end # @image.each_row
rlm@11 223 debug "end tile row #{ytile}"
rlm@11 224 end # @config[:repeat][i].times
rlm@11 225
rlm@11 226 gcode.epilogue
rlm@11 227 end # def render_tiled_image
rlm@11 228
rlm@11 229 # cut out the tile with bottom left at x,y
rlm@11 230 def render_cut gcode, x, y
rlm@11 231 radius = @config[:corner_radius]
rlm@11 232 left = x
rlm@11 233 bottom = y
rlm@11 234 right = x+@config[:tile_size][0]
rlm@11 235 top = y+@config[:tile_size][1]
rlm@11 236
rlm@11 237 gcode.instance_eval do
rlm@11 238 if radius && radius > 0
rlm@11 239 jog :x => left, :y => bottom+radius
rlm@11 240 move :x => left, :y => top-radius, :laser => true
rlm@11 241 turn_cw :x => left+radius, :y => top, :i => radius
rlm@11 242 move :x => right-radius, :y => top
rlm@11 243 turn_cw :x => right, :y => top-radius, :j => -radius
rlm@11 244 move :x => right, :y => bottom+radius
rlm@11 245 turn_cw :x => right-radius, :y => bottom, :i => -radius
rlm@11 246 move :x => left+radius, :y => bottom
rlm@11 247 turn_cw :x => left, :y => bottom+radius, :j => radius
rlm@11 248 nc :laser => false
rlm@11 249 else
rlm@11 250 jog :x => left, :y => bottom
rlm@11 251 move :x => left, :y => top, :laser => true
rlm@11 252 move :x => right, :y => top
rlm@11 253 move :x => right, :y => bottom
rlm@11 254 move :x => left, :y => bottom
rlm@11 255 nc :laser => false
rlm@11 256 end
rlm@11 257 end
rlm@11 258 end
rlm@11 259
rlm@11 260 # render gcode to cut out the tiles
rlm@11 261 def render_all_cuts gcode
rlm@11 262 gcode.preamble :feed => @config[:cut_feed]
rlm@11 263 @config[:repeat][1].times do |ytile|
rlm@11 264 @config[:repeat][0].times do |xtile|
rlm@11 265 render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0)
rlm@11 266 end
rlm@11 267 end
rlm@11 268 gcode.epilogue
rlm@11 269 end
rlm@11 270
rlm@11 271 def render_all gcode, gmask, cuts
rlm@11 272 render_tiled_image gcode, gmask
rlm@11 273 render_all_cuts cuts
rlm@11 274 end
rlm@11 275
rlm@11 276 def open_gcode_file &block
rlm@11 277 io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block
rlm@11 278 end
rlm@11 279
rlm@11 280 def open_gmask_file &block
rlm@11 281 io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block
rlm@11 282 end
rlm@11 283
rlm@11 284 def open_cut_file &block
rlm@11 285 io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block
rlm@11 286 end
rlm@11 287
rlm@11 288 def generate_all_files
rlm@11 289 open_gcode_file do |gcode|
rlm@11 290 open_gmask_file do |gmask|
rlm@11 291 render_tiled_image gcode, gmask
rlm@11 292 end
rlm@11 293 end
rlm@11 294
rlm@11 295 open_cut_file do |cut|
rlm@11 296 render_all_cuts cut
rlm@11 297 end
rlm@11 298 end
rlm@11 299
rlm@11 300 def config_to_yaml
rlm@11 301 @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join
rlm@11 302 end
rlm@11 303
rlm@11 304 def debug msg
rlm@11 305 STDERR.puts msg if @debug
rlm@11 306 end
rlm@11 307
rlm@11 308 def initialize opts={}
rlm@11 309 self.config = DEFAULTS.dup
rlm@11 310
rlm@11 311 if opts[:config_file]
rlm@11 312 self.merge_config load_config_file opts[:config_file]
rlm@11 313 elsif opts[:default_config_file] && c = try_load_default_config_file
rlm@11 314 self.merge_config c
rlm@11 315 end
rlm@11 316
rlm@11 317 self.merge_config opts[:config] if opts[:config]
rlm@11 318
rlm@11 319 @debug = opts[:debug]
rlm@11 320
rlm@11 321 if opts[:image]
rlm@11 322 image = opts[:image]
rlm@11 323 elsif opts[:image_file]
rlm@11 324 load_image_file opts[:image_file]
rlm@11 325 end
rlm@11 326 end
rlm@11 327
rlm@11 328 end # class Graster