Mercurial > lasercutter
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 ruby3 require 'rubygems'4 require 'yaml'5 require 'RMagick'7 class Graster9 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 device54 :on_range => [0.0,0.5], # Luminosity range for which the laser should be on55 :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 decelerating57 # 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 from62 # the size of the bitmap.63 :tile_spacing => [0.125,0.125], # X,Y gap between repeated tiles in inches64 :feed => 120, # Speed to move the X axis while burning, in inches/minute65 :cut_feed => 20, # Speed at which to cut out tiles66 :corner_radius => 0 # Radius of rounded corners for cutout, 0 for pointy corners67 }69 class InvalidConfig < Exception; end70 def update_config71 @scale = @config[:dpi].map{|n| 1.0/n }72 @offset = @config[:offset]74 if @image75 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_interval81 end83 @on_range = Range.new Image.f_to_pix(@config[:on_range].first),84 Image.f_to_pix(@config[:on_range].last)85 end87 def validate_config88 raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"89 end91 def config= h92 @config = {}93 DEFAULTS.each {|k,v| @config[k] = h[k] || v }94 update_config95 return h96 end98 def merge_config h99 @config ||= DEFAULTS.dup100 h.each {|k,v| @config[k] = v if DEFAULTS[k] }101 update_config102 return h103 end105 attr_reader :config107 def image= img108 debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels"109 @image = img110 @image.build_spans @on_range111 update_config112 build_tiled_rows113 return img114 end116 attr_reader :image118 def try_load_config_file pn119 if File.exist?(pn)120 c = {}121 YAML.load_file(pn).each {|k,v| c[k.intern] = v }122 return c123 end124 end126 def try_load_default_config_file127 try_load_config_file './graster.yml'128 end130 def load_config_file pn131 try_load_config_file pn or raise "config file not found '#{pn}'"132 end134 def load_image_file pn135 self.image = Image.from_file(pn)136 end138 # convert tile + pixel coordinates to inches139 def axis_inches axis, tile, pixel140 @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]141 end143 def x_inches tile, pixel144 axis_inches 0, tile, pixel145 end147 def y_inches tile, pixel148 axis_inches 1, tile, pixel149 end151 # return a complete tiled row of spans converted to inches152 def tiled_row_spans y, forward=true153 spans = @image.spans[y]154 return spans if spans.empty?155 tiled_spans = []157 if forward158 @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 end162 end163 else164 (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 end168 end169 end171 return tiled_spans172 end174 def build_tiled_rows175 forward = false176 @tiled_rows = []177 @image.size[1].times {|y| @tiled_rows << tiled_row_spans(y, (forward = !forward)) }178 end180 # generate a unique id for this job181 def job_hash182 [@image,@config].hash183 end185 # render a complete tiled image to gcode and gmask streams186 def render_tiled_image gcode, gmask187 debug "rendering tiled image"188 job_id = job_hash189 hyst = -@scale[0]/2190 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 => true195 gmask.preamble197 @config[:repeat][1].times do |ytile|198 debug "begin tile row #{ytile}"199 ypix = 0200 (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 : -1207 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 => yinches210 gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches211 gmask.begin_row forward212 # G0 X0.606 Y1.976213 # G1 X3.396 Y1.976214 # G0 X3.392 Y1.978215 # G1 X0.610 Y1.978216 # G0 X0.614 Y1.980217 # G1 X3.388 Y1.980219 spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }220 end # unless spans.empty?221 ypix += 1222 end # @image.each_row223 debug "end tile row #{ytile}"224 end # @config[:repeat][i].times226 gcode.epilogue227 end # def render_tiled_image229 # cut out the tile with bottom left at x,y230 def render_cut gcode, x, y231 radius = @config[:corner_radius]232 left = x233 bottom = y234 right = x+@config[:tile_size][0]235 top = y+@config[:tile_size][1]237 gcode.instance_eval do238 if radius && radius > 0239 jog :x => left, :y => bottom+radius240 move :x => left, :y => top-radius, :laser => true241 turn_cw :x => left+radius, :y => top, :i => radius242 move :x => right-radius, :y => top243 turn_cw :x => right, :y => top-radius, :j => -radius244 move :x => right, :y => bottom+radius245 turn_cw :x => right-radius, :y => bottom, :i => -radius246 move :x => left+radius, :y => bottom247 turn_cw :x => left, :y => bottom+radius, :j => radius248 nc :laser => false249 else250 jog :x => left, :y => bottom251 move :x => left, :y => top, :laser => true252 move :x => right, :y => top253 move :x => right, :y => bottom254 move :x => left, :y => bottom255 nc :laser => false256 end257 end258 end260 # render gcode to cut out the tiles261 def render_all_cuts gcode262 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 end267 end268 gcode.epilogue269 end271 def render_all gcode, gmask, cuts272 render_tiled_image gcode, gmask273 render_all_cuts cuts274 end276 def open_gcode_file &block277 io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block278 end280 def open_gmask_file &block281 io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block282 end284 def open_cut_file &block285 io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block286 end288 def generate_all_files289 open_gcode_file do |gcode|290 open_gmask_file do |gmask|291 render_tiled_image gcode, gmask292 end293 end295 open_cut_file do |cut|296 render_all_cuts cut297 end298 end300 def config_to_yaml301 @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join302 end304 def debug msg305 STDERR.puts msg if @debug306 end308 def initialize opts={}309 self.config = DEFAULTS.dup311 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_file314 self.merge_config c315 end317 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 end326 end328 end # class Graster