Mercurial > lasercutter
diff 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 diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/graster/graster/lib/graster.rb Tue Aug 24 19:06:45 2010 -0400 1.3 @@ -0,0 +1,328 @@ 1.4 +#!/usr/bin/env ruby 1.5 + 1.6 +require 'rubygems' 1.7 +require 'yaml' 1.8 +require 'RMagick' 1.9 + 1.10 +class Graster 1.11 + 1.12 + autoload :Runner, File.join(File.dirname(__FILE__), 'graster', 'runner') 1.13 + autoload :Image, File.join(File.dirname(__FILE__), 'graster', 'image') 1.14 + autoload :GcodeFile, File.join(File.dirname(__FILE__), 'graster', 'gcode_file') 1.15 + autoload :GmaskFile, File.join(File.dirname(__FILE__), 'graster', 'gmask_file') 1.16 + 1.17 + ROOT2 = Math.sqrt(2) 1.18 + 1.19 + OPTIONS = { 1.20 + :dpi => [[Float],"X,Y","Dots per inch of your device"], 1.21 + :on_range => [[Float], 1.22 + "MIN,MAX","Luminosity range for which the", 1.23 + "laser should be on"], 1.24 + :overshoot => [Float,"INCHES", 1.25 + "Distance the X axis should travel", 1.26 + "past the outer boundaries of the outer", 1.27 + "images. This needs to be wide enough", 1.28 + "so that the X axis doesn't start", 1.29 + "decelerating until after it has", 1.30 + "cleared the image"], 1.31 + :offset => [[Float],"X,Y", 1.32 + "Location for the bottom left corner", 1.33 + "of the bottom left tile. The X", 1.34 + "component of this setting must be", 1.35 + "equal to or greater than overshoot"], 1.36 + :repeat => [[Integer],"X,Y", 1.37 + "Number of times to repeat the image", 1.38 + "in the X and Y axes, respectively.", 1.39 + "Size of the tile(s) inches. Any nil", 1.40 + "value is calculated from the size of", 1.41 + "the bitmap"], 1.42 + :tile_spacing => [[Float],"X,Y", 1.43 + "X,Y gap between repeated tiles in", 1.44 + "inches"], 1.45 + :feed => [Float,"N", 1.46 + "Speed to move the X axis while", 1.47 + "burning, in inches/minute"], 1.48 + :cut_feed => [Float,"N", 1.49 + "Speed at which to cut out tiles"], 1.50 + :corner_radius => [Float,"N", 1.51 + "Radius of rounded corners for", 1.52 + "cutout, 0 for pointy corners"] 1.53 + } 1.54 + 1.55 + DEFAULTS = { 1.56 + :dpi => [500,500], # X,Y dots per inch of your device 1.57 + :on_range => [0.0,0.5], # Luminosity range for which the laser should be on 1.58 + :overshoot => 0.5, # Distance the X axis should travel past the outer boundaries of the outer images. 1.59 + # This needs to be wide enough so that the X axis doesn't start decelerating 1.60 + # until after it has cleared the image. 1.61 + :offset => [1.0,1.0], # X,Y location for the bottom left corner of the bottom left tile. 1.62 + # The X component of this setting must be equal to or greater than :overshoot. 1.63 + :repeat => [1,1], # Number of times to repeat the image in the X and Y axes, respectively. 1.64 + :tile_size => [false,false], # Size of the tile(s) inches. Any nil value is calculated from 1.65 + # the size of the bitmap. 1.66 + :tile_spacing => [0.125,0.125], # X,Y gap between repeated tiles in inches 1.67 + :feed => 120, # Speed to move the X axis while burning, in inches/minute 1.68 + :cut_feed => 20, # Speed at which to cut out tiles 1.69 + :corner_radius => 0 # Radius of rounded corners for cutout, 0 for pointy corners 1.70 + } 1.71 + 1.72 + class InvalidConfig < Exception; end 1.73 + def update_config 1.74 + @scale = @config[:dpi].map{|n| 1.0/n } 1.75 + @offset = @config[:offset] 1.76 + 1.77 + if @image 1.78 + 2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] } 1.79 + @tile_interval = [] 1.80 + 2.times {|i| 1.81 + @tile_interval << @config[:tile_size][i] + @config[:tile_spacing][i] 1.82 + } 1.83 + @tile_interval 1.84 + end 1.85 + 1.86 + @on_range = Range.new Image.f_to_pix(@config[:on_range].first), 1.87 + Image.f_to_pix(@config[:on_range].last) 1.88 + end 1.89 + 1.90 + def validate_config 1.91 + raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})" 1.92 + end 1.93 + 1.94 + def config= h 1.95 + @config = {} 1.96 + DEFAULTS.each {|k,v| @config[k] = h[k] || v } 1.97 + update_config 1.98 + return h 1.99 + end 1.100 + 1.101 + def merge_config h 1.102 + @config ||= DEFAULTS.dup 1.103 + h.each {|k,v| @config[k] = v if DEFAULTS[k] } 1.104 + update_config 1.105 + return h 1.106 + end 1.107 + 1.108 + attr_reader :config 1.109 + 1.110 + def image= img 1.111 + debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels" 1.112 + @image = img 1.113 + @image.build_spans @on_range 1.114 + update_config 1.115 + build_tiled_rows 1.116 + return img 1.117 + end 1.118 + 1.119 + attr_reader :image 1.120 + 1.121 + def try_load_config_file pn 1.122 + if File.exist?(pn) 1.123 + c = {} 1.124 + YAML.load_file(pn).each {|k,v| c[k.intern] = v } 1.125 + return c 1.126 + end 1.127 + end 1.128 + 1.129 + def try_load_default_config_file 1.130 + try_load_config_file './graster.yml' 1.131 + end 1.132 + 1.133 + def load_config_file pn 1.134 + try_load_config_file pn or raise "config file not found '#{pn}'" 1.135 + end 1.136 + 1.137 + def load_image_file pn 1.138 + self.image = Image.from_file(pn) 1.139 + end 1.140 + 1.141 + # convert tile + pixel coordinates to inches 1.142 + def axis_inches axis, tile, pixel 1.143 + @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis] 1.144 + end 1.145 + 1.146 + def x_inches tile, pixel 1.147 + axis_inches 0, tile, pixel 1.148 + end 1.149 + 1.150 + def y_inches tile, pixel 1.151 + axis_inches 1, tile, pixel 1.152 + end 1.153 + 1.154 + # return a complete tiled row of spans converted to inches 1.155 + def tiled_row_spans y, forward=true 1.156 + spans = @image.spans[y] 1.157 + return spans if spans.empty? 1.158 + tiled_spans = [] 1.159 + 1.160 + if forward 1.161 + @config[:repeat][0].times do |tile| 1.162 + spans.each do |span| 1.163 + tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])] 1.164 + end 1.165 + end 1.166 + else 1.167 + (0...@config[:repeat][0]).to_a.reverse.each do |tile| 1.168 + spans.reverse.each do |span| 1.169 + tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])] 1.170 + end 1.171 + end 1.172 + end 1.173 + 1.174 + return tiled_spans 1.175 + end 1.176 + 1.177 + def build_tiled_rows 1.178 + forward = false 1.179 + @tiled_rows = [] 1.180 + @image.size[1].times {|y| @tiled_rows << tiled_row_spans(y, (forward = !forward)) } 1.181 + end 1.182 + 1.183 + # generate a unique id for this job 1.184 + def job_hash 1.185 + [@image,@config].hash 1.186 + end 1.187 + 1.188 + # render a complete tiled image to gcode and gmask streams 1.189 + def render_tiled_image gcode, gmask 1.190 + debug "rendering tiled image" 1.191 + job_id = job_hash 1.192 + hyst = -@scale[0]/2 1.193 + gcode.comment "raster gcode for job #{job_id}" 1.194 + gcode.comment "image: #{@image.filename} #{@image.size.inspect}" 1.195 + gcode.comment "config: #{@config.inspect}" 1.196 + 1.197 + gcode.preamble :feed => @config[:feed], :mask => true 1.198 + gmask.preamble 1.199 + 1.200 + @config[:repeat][1].times do |ytile| 1.201 + debug "begin tile row #{ytile}" 1.202 + ypix = 0 1.203 + (0...@tiled_rows).each do |spans| 1.204 + debug "pixel row #{ypix} is empty" if spans.empty? 1.205 + unless spans.empty? 1.206 + yinches = y_inches(ytile, ypix) 1.207 + forward = spans[0][0] < spans[-1][1] 1.208 + dir = forward ? 1 : -1 1.209 + 1.210 + debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans" 1.211 + 1.212 + gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches 1.213 + gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches 1.214 + gmask.begin_row forward 1.215 + # G0 X0.606 Y1.976 1.216 + # G1 X3.396 Y1.976 1.217 + # G0 X3.392 Y1.978 1.218 + # G1 X0.610 Y1.978 1.219 + # G0 X0.614 Y1.980 1.220 + # G1 X3.388 Y1.980 1.221 + 1.222 + spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst } 1.223 + end # unless spans.empty? 1.224 + ypix += 1 1.225 + end # @image.each_row 1.226 + debug "end tile row #{ytile}" 1.227 + end # @config[:repeat][i].times 1.228 + 1.229 + gcode.epilogue 1.230 + end # def render_tiled_image 1.231 + 1.232 + # cut out the tile with bottom left at x,y 1.233 + def render_cut gcode, x, y 1.234 + radius = @config[:corner_radius] 1.235 + left = x 1.236 + bottom = y 1.237 + right = x+@config[:tile_size][0] 1.238 + top = y+@config[:tile_size][1] 1.239 + 1.240 + gcode.instance_eval do 1.241 + if radius && radius > 0 1.242 + jog :x => left, :y => bottom+radius 1.243 + move :x => left, :y => top-radius, :laser => true 1.244 + turn_cw :x => left+radius, :y => top, :i => radius 1.245 + move :x => right-radius, :y => top 1.246 + turn_cw :x => right, :y => top-radius, :j => -radius 1.247 + move :x => right, :y => bottom+radius 1.248 + turn_cw :x => right-radius, :y => bottom, :i => -radius 1.249 + move :x => left+radius, :y => bottom 1.250 + turn_cw :x => left, :y => bottom+radius, :j => radius 1.251 + nc :laser => false 1.252 + else 1.253 + jog :x => left, :y => bottom 1.254 + move :x => left, :y => top, :laser => true 1.255 + move :x => right, :y => top 1.256 + move :x => right, :y => bottom 1.257 + move :x => left, :y => bottom 1.258 + nc :laser => false 1.259 + end 1.260 + end 1.261 + end 1.262 + 1.263 + # render gcode to cut out the tiles 1.264 + def render_all_cuts gcode 1.265 + gcode.preamble :feed => @config[:cut_feed] 1.266 + @config[:repeat][1].times do |ytile| 1.267 + @config[:repeat][0].times do |xtile| 1.268 + render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0) 1.269 + end 1.270 + end 1.271 + gcode.epilogue 1.272 + end 1.273 + 1.274 + def render_all gcode, gmask, cuts 1.275 + render_tiled_image gcode, gmask 1.276 + render_all_cuts cuts 1.277 + end 1.278 + 1.279 + def open_gcode_file &block 1.280 + io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block 1.281 + end 1.282 + 1.283 + def open_gmask_file &block 1.284 + io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block 1.285 + end 1.286 + 1.287 + def open_cut_file &block 1.288 + io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block 1.289 + end 1.290 + 1.291 + def generate_all_files 1.292 + open_gcode_file do |gcode| 1.293 + open_gmask_file do |gmask| 1.294 + render_tiled_image gcode, gmask 1.295 + end 1.296 + end 1.297 + 1.298 + open_cut_file do |cut| 1.299 + render_all_cuts cut 1.300 + end 1.301 + end 1.302 + 1.303 + def config_to_yaml 1.304 + @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join 1.305 + end 1.306 + 1.307 + def debug msg 1.308 + STDERR.puts msg if @debug 1.309 + end 1.310 + 1.311 + def initialize opts={} 1.312 + self.config = DEFAULTS.dup 1.313 + 1.314 + if opts[:config_file] 1.315 + self.merge_config load_config_file opts[:config_file] 1.316 + elsif opts[:default_config_file] && c = try_load_default_config_file 1.317 + self.merge_config c 1.318 + end 1.319 + 1.320 + self.merge_config opts[:config] if opts[:config] 1.321 + 1.322 + @debug = opts[:debug] 1.323 + 1.324 + if opts[:image] 1.325 + image = opts[:image] 1.326 + elsif opts[:image_file] 1.327 + load_image_file opts[:image_file] 1.328 + end 1.329 + end 1.330 + 1.331 +end # class Graster