Class: Graster

Inherits:
Object
  • Object
show all
Defined in:
lib/graster.rb,
lib/graster/image.rb,
lib/graster/runner.rb,
lib/graster/gcode_file.rb,
lib/graster/gmask_file.rb

Defined Under Namespace

Classes: GcodeFile, GmaskFile, Image, InvalidConfig, Runner

Constant Summary collapse

ROOT2 =
Math.sqrt(2)
OPTIONS =
{
  :dpi =>             [[Float],"X,Y","Dots per inch of your device"],
  :on_range =>        [[Float],
    "MIN,MAX","Luminosity range for which the",
    "laser should be on"],
  :overshoot =>       [Float,"INCHES",
    "Distance the X axis should travel",
    "past the outer boundaries of the outer",
    "images. This needs to be wide enough",
    "so that the X axis doesn't start",
    "decelerating until after it has",
    "cleared the image"],
  :offset =>          [[Float],"X,Y",
    "Location for the bottom left corner",
    "of the bottom left tile. The X",
    "component of this setting must be",
    "equal to or greater than overshoot"],
  :repeat =>          [[Integer],"X,Y",
    "Number of times to repeat the image",
    "in the X and Y axes, respectively.",
    "Size of the tile(s) inches. Any nil",
    "value is calculated from the size of",
    "the bitmap"],
  :tile_spacing =>    [[Float],"X,Y",
    "X,Y gap between repeated tiles in",
    "inches"],
  :feed =>            [Float,"N",
    "Speed to move the X axis while",
    "burning, in inches/minute"],
  :cut_feed =>        [Float,"N",
    "Speed at which to cut out tiles"],
  :corner_radius =>   [Float,"N",
    "Radius of rounded corners for",
    "cutout, 0 for pointy corners"]
}
DEFAULTS =
{
  :dpi => [500,500],                 # X,Y dots per inch of your device
  :on_range =>  [0.0,0.5],           # Luminosity range for which the laser should be on
  :overshoot => 0.5,                 # Distance the X axis should travel past the outer boundaries of the outer images.
                                     # This needs to be wide enough so that the X axis doesn't start decelerating
                                     # until after it has cleared the image.
  :offset => [1.0,1.0],              # X,Y location for the bottom left corner of the bottom left tile.
                                     # The X component of this setting must be equal to or greater than :overshoot.
  :repeat => [1,1],                  # Number of times to repeat the image in the X and Y axes, respectively.
  :tile_size => [false,false],       # Size of the tile(s) inches. Any nil value is calculated from
                                     # the size of the bitmap.
  :tile_spacing => [0.125,0.125],    # X,Y gap between repeated tiles in inches
  :feed => 120,                      # Speed to move the X axis while burning, in inches/minute
  :cut_feed => 20,                   # Speed at which to cut out tiles
  :corner_radius => 0                # Radius of rounded corners for cutout, 0 for pointy corners
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Graster

Returns a new instance of Graster.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/graster.rb', line 301

def initialize opts={}
  self.config = DEFAULTS.dup
    
  if opts[:config_file]
    self.merge_config load_config_file opts[:config_file]
  elsif opts[:default_config_file] && c = try_load_default_config_file
    self.merge_config c
  end

  self.merge_config opts[:config] if opts[:config]

  @debug = opts[:debug]

  if opts[:image]
    image = opts[:image]
  elsif opts[:image_file]
    load_image_file opts[:image_file]
  end
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



105
106
107
# File 'lib/graster.rb', line 105

def config
  @config
end

#imageObject

Returns the value of attribute image.



116
117
118
# File 'lib/graster.rb', line 116

def image
  @image
end

Instance Method Details

#axis_inches(axis, tile, pixel) ⇒ Object

convert tile + pixel coordinates to inches



139
140
141
# File 'lib/graster.rb', line 139

def axis_inches axis, tile, pixel
  @offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]
end

#build_tiled_rowsObject



174
175
176
177
178
# File 'lib/graster.rb', line 174

def build_tiled_rows
  forward = false
  @tiled_rows = []
  @image.size[1].times {|y| @tiled_rows << tiled_row_spans(y, (forward = !forward)) }
end

#config_to_yamlObject



293
294
295
# File 'lib/graster.rb', line 293

def config_to_yaml
  @config.map {|k,v| "#{k}: #{v.inspect}\n" }.join
end

#debug(msg) ⇒ Object



297
298
299
# File 'lib/graster.rb', line 297

def debug msg
  STDERR.puts msg if @debug
end

#generate_all_filesObject



281
282
283
284
285
286
287
288
289
290
291
# File 'lib/graster.rb', line 281

def generate_all_files
  open_gcode_file do |gcode|
    open_gmask_file do |gmask|
      render_tiled_image gcode, gmask
    end
  end

  open_cut_file do |cut|
    render_all_cuts cut
  end
end

#job_hashObject

generate a unique id for this job



181
182
183
# File 'lib/graster.rb', line 181

def job_hash
  [@image,@config].hash
end

#load_config_file(pn) ⇒ Object



130
131
132
# File 'lib/graster.rb', line 130

def load_config_file pn
  try_load_config_file pn or raise "config file not found '#{pn}'"
end

#load_image_file(pn) ⇒ Object



134
135
136
# File 'lib/graster.rb', line 134

def load_image_file pn
  self.image = Image.from_file(pn)
end

#merge_config(h) ⇒ Object



98
99
100
101
102
103
# File 'lib/graster.rb', line 98

def merge_config h
  @config ||= DEFAULTS.dup
  h.each {|k,v| @config[k] = v if DEFAULTS[k] }
  update_config
  return h
end

#open_cut_file(&block) ⇒ Object



277
278
279
# File 'lib/graster.rb', line 277

def open_cut_file &block
  io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block
end

#open_gcode_file(&block) ⇒ Object



269
270
271
# File 'lib/graster.rb', line 269

def open_gcode_file &block
  io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block
end

#open_gmask_file(&block) ⇒ Object



273
274
275
# File 'lib/graster.rb', line 273

def open_gmask_file &block
  io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block
end

#render_all(gcode, gmask, cuts) ⇒ Object



264
265
266
267
# File 'lib/graster.rb', line 264

def render_all gcode, gmask, cuts
  render_tiled_image gcode, gmask
  render_all_cuts cuts
end

#render_all_cuts(gcode) ⇒ Object

render gcode to cut out the tiles



254
255
256
257
258
259
260
261
262
# File 'lib/graster.rb', line 254

def render_all_cuts gcode
  gcode.preamble :feed => @config[:cut_feed]
  @config[:repeat][1].times do |ytile|
    @config[:repeat][0].times do |xtile|
      render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0)
    end
  end
  gcode.epilogue
end

#render_cut(gcode, x, y) ⇒ Object

cut out the tile with bottom left at x,y



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/graster.rb', line 223

def render_cut gcode, x, y
  radius = @config[:corner_radius]
  left = x
  bottom = y
  right = x+@config[:tile_size][0]
  top = y+@config[:tile_size][1]

  gcode.instance_eval do
    if radius && radius > 0
      jog :x => left, :y => bottom+radius
      move :x => left, :y => top-radius, :laser => true
      turn_cw :x => left+radius, :y => top, :i => radius
      move :x => right-radius, :y => top
      turn_cw :x => right, :y => top-radius, :j => -radius
      move :x => right, :y => bottom+radius
      turn_cw :x => right-radius, :y => bottom, :i => -radius
      move :x => left+radius, :y => bottom
      turn_cw :x => left, :y => bottom+radius, :j => radius
      nc :laser => false
    else
      jog :x => left, :y => bottom
      move :x => left, :y => top, :laser => true
      move :x => right, :y => top
      move :x => right, :y => bottom
      move :x => left, :y => bottom
      nc :laser => false
    end
  end
end

#render_tiled_image(gcode, gmask) ⇒ Object

render a complete tiled image to gcode and gmask streams



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/graster.rb', line 186

def render_tiled_image gcode, gmask
  debug "rendering tiled image"
  job_id = job_hash
  hyst = -@scale[0]/2
  gcode.comment "raster gcode for job #{job_id}"
  gcode.comment "image: #{@image.filename} #{@image.size.inspect}"
  gcode.comment "config: #{@config.inspect}"

  gcode.preamble :feed => @config[:feed], :mask => true
  gmask.preamble

  @config[:repeat][1].times do |ytile|
    debug "begin tile row #{ytile}"
    ypix = 0
    (0...@tiled_rows).each do |spans|
      debug "pixel row #{ypix} is empty" if spans.empty?
      unless spans.empty?
        yinches = y_inches(ytile, ypix)
        forward = spans[0][0] < spans[-1][1]
        dir = forward ? 1 : -1

        debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans"

        gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches
        gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches
        gmask.begin_row forward
        spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }
      end # unless spans.empty?
      ypix += 1
    end # @image.each_row
    debug "end tile row #{ytile}"
  end # @config[:repeat][i].times

  gcode.epilogue
end

#tiled_row_spans(y, forward = true) ⇒ Object

return a complete tiled row of spans converted to inches



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/graster.rb', line 152

def tiled_row_spans y, forward=true
  spans = @image.spans[y]
  return spans if spans.empty?
  tiled_spans = []

  if forward
    @config[:repeat][0].times do |tile|
      spans.each do |span|
        tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])]
      end
    end
  else
    (0...@config[:repeat][0]).to_a.reverse.each do |tile|
      spans.reverse.each do |span|
        tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])]
      end
    end
  end

  return tiled_spans
end

#try_load_config_file(pn) ⇒ Object



118
119
120
121
122
123
124
# File 'lib/graster.rb', line 118

def try_load_config_file pn
  if File.exist?(pn)
    c = {}
    YAML.load_file(pn).each {|k,v| c[k.intern] = v }
    return c
  end
end

#try_load_default_config_fileObject



126
127
128
# File 'lib/graster.rb', line 126

def try_load_default_config_file
  try_load_config_file './graster.yml'
end

#update_configObject



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/graster.rb', line 70

def update_config
  @scale = @config[:dpi].map{|n| 1.0/n }
  @offset = @config[:offset]

  if @image
    2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] }
    @tile_interval = []
    2.times {|i|
      @tile_interval << @config[:tile_size][i] + @config[:tile_spacing][i]
    }
    @tile_interval
  end

  @on_range = Range.new Image.f_to_pix(@config[:on_range].first),
                        Image.f_to_pix(@config[:on_range].last)
end

#validate_configObject



87
88
89
# File 'lib/graster.rb', line 87

def validate_config
  raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"
end

#x_inches(tile, pixel) ⇒ Object



143
144
145
# File 'lib/graster.rb', line 143

def x_inches tile, pixel
  axis_inches 0, tile, pixel
end

#y_inches(tile, pixel) ⇒ Object



147
148
149
# File 'lib/graster.rb', line 147

def y_inches tile, pixel
  axis_inches 1, tile, pixel
end