graster 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/bin/graster +6 -0
- data/bin/gtile +74 -0
- data/lib/graster.rb +316 -0
- data/lib/graster/gcode_file.rb +71 -0
- data/lib/graster/gmask_file.rb +24 -0
- data/lib/graster/image.rb +83 -0
- data/lib/graster/runner.rb +66 -0
- metadata +74 -0
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |s|
|
4
|
+
s.name = "graster"
|
5
|
+
s.description = s.summary = "G Raster!"
|
6
|
+
s.email = "joshbuddy@gmail.com"
|
7
|
+
s.homepage = "http://github.com/joshbuddy/graster"
|
8
|
+
s.authors = ["Jedediah Smith", "Joshua Hull"]
|
9
|
+
s.files = FileList["[A-Z]*", "{lib,bin}/**/*"]
|
10
|
+
s.add_dependency 'RMagick'
|
11
|
+
s.rubyforge_project = 'graster'
|
12
|
+
end
|
13
|
+
Jeweler::GemcutterTasks.new
|
14
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
15
|
+
rubyforge.doc_task = "rdoc"
|
16
|
+
rubyforge.remote_doc_path = ''
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/graster
ADDED
data/bin/gtile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
# Tokyo cache cow command line interface script.
|
3
|
+
# Run <tt>tokyo_cache_cow -h</tt> to get more usage.
|
4
|
+
require File.dirname(__FILE__) + '/../lib/graster'
|
5
|
+
|
6
|
+
unless ARGV.size == 5
|
7
|
+
puts "usage: ruby tile.rb <input-gcode-file> <tile-width> <tile-height> <horiz-count> <vert-count>"
|
8
|
+
exit(1)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse_line line
|
12
|
+
nc = {}
|
13
|
+
line.gsub(/\([^)]*\)/,'').upcase.scan(/([A-Z])\s*([0-9\.]+)?/).each {|code| nc[code[0].intern] = (code[1] && code[1].to_f) }
|
14
|
+
nc
|
15
|
+
end
|
16
|
+
|
17
|
+
def gcode ncs
|
18
|
+
ncs = [ncs] unless ncs.is_a? Array
|
19
|
+
ncs.reduce('') {|a,nc| a << (nc.map {|k,v| "#{k}#{v}" }.join(' ') + "\n") }
|
20
|
+
end
|
21
|
+
|
22
|
+
tile_width = ARGV[1].to_f
|
23
|
+
tile_height = ARGV[2].to_f
|
24
|
+
horiz_count = ARGV[3].to_i
|
25
|
+
vert_count = ARGV[4].to_i
|
26
|
+
|
27
|
+
header = []
|
28
|
+
body = []
|
29
|
+
footer = []
|
30
|
+
state = :header
|
31
|
+
|
32
|
+
|
33
|
+
File.open ARGV[0] do |io|
|
34
|
+
io.each_line do |line|
|
35
|
+
if (nc = parse_line(line)) != {}
|
36
|
+
case state
|
37
|
+
when :header
|
38
|
+
if nc[:G] == 0 || nc[:G] == 1
|
39
|
+
state = :body
|
40
|
+
body << nc
|
41
|
+
else
|
42
|
+
header << nc
|
43
|
+
end
|
44
|
+
|
45
|
+
when :body
|
46
|
+
if nc[:G] == 0 || nc[:G] == 1
|
47
|
+
body << nc
|
48
|
+
else
|
49
|
+
state = :footer
|
50
|
+
footer << nc
|
51
|
+
end
|
52
|
+
|
53
|
+
when :footer
|
54
|
+
footer << nc
|
55
|
+
end
|
56
|
+
end # case
|
57
|
+
|
58
|
+
end # io.each_line
|
59
|
+
end # File.open
|
60
|
+
|
61
|
+
print gcode(header)
|
62
|
+
|
63
|
+
vert_count.times.map do |yc|
|
64
|
+
horiz_count.times.map do |xc|
|
65
|
+
body.each do |nc|
|
66
|
+
nc = nc.dup
|
67
|
+
nc[:X] += xc*tile_width if nc[:X]
|
68
|
+
nc[:Y] += yc*tile_height if nc[:Y]
|
69
|
+
print gcode(nc)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
print gcode(footer)
|
data/lib/graster.rb
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'yaml'
|
5
|
+
require 'RMagick'
|
6
|
+
|
7
|
+
class Graster
|
8
|
+
|
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')
|
13
|
+
|
14
|
+
ROOT2 = Math.sqrt(2)
|
15
|
+
|
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
|
+
}
|
51
|
+
|
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
|
+
}
|
68
|
+
|
69
|
+
class InvalidConfig < Exception; end
|
70
|
+
def update_config
|
71
|
+
@scale = @config[:dpi].map{|n| 1.0/n }
|
72
|
+
@offset = @config[:offset]
|
73
|
+
|
74
|
+
if @image
|
75
|
+
2.times {|i| @config[:tile_size][i] ||= @image.size[i]*@scale[i] }
|
76
|
+
@tile_interval = 2.times.map {|i|
|
77
|
+
@config[:tile_size][i] + @config[:tile_spacing][i]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
@on_range = Range.new Image.f_to_pix(@config[:on_range].first),
|
82
|
+
Image.f_to_pix(@config[:on_range].last)
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_config
|
86
|
+
raise InvalidConfig.new "X offset (#{@config[:offset][0]}) must be greater or equal to overshoot (#{@config[:overshoot]})"
|
87
|
+
end
|
88
|
+
|
89
|
+
def config= h
|
90
|
+
@config = {}
|
91
|
+
DEFAULTS.each {|k,v| @config[k] = h[k] || v }
|
92
|
+
update_config
|
93
|
+
return h
|
94
|
+
end
|
95
|
+
|
96
|
+
def merge_config h
|
97
|
+
@config ||= DEFAULTS.dup
|
98
|
+
h.each {|k,v| @config[k] = v if DEFAULTS[k] }
|
99
|
+
update_config
|
100
|
+
return h
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :config
|
104
|
+
|
105
|
+
def image= img
|
106
|
+
debug "image set to #{img.filename} #{img.size.inspect} #{img.pixels.size} pixels"
|
107
|
+
@image = img
|
108
|
+
@image.build_spans @on_range
|
109
|
+
update_config
|
110
|
+
build_tiled_rows
|
111
|
+
return img
|
112
|
+
end
|
113
|
+
|
114
|
+
attr_reader :image
|
115
|
+
|
116
|
+
def try_load_config_file pn
|
117
|
+
if File.exist?(pn)
|
118
|
+
c = {}
|
119
|
+
YAML.load_file(pn).each {|k,v| c[k.intern] = v }
|
120
|
+
return c
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def try_load_default_config_file
|
125
|
+
try_load_config_file './graster.yml'
|
126
|
+
end
|
127
|
+
|
128
|
+
def load_config_file pn
|
129
|
+
try_load_config_file pn or raise "config file not found '#{pn}'"
|
130
|
+
end
|
131
|
+
|
132
|
+
def load_image_file pn
|
133
|
+
self.image = Image.from_file(pn)
|
134
|
+
end
|
135
|
+
|
136
|
+
# convert tile + pixel coordinates to inches
|
137
|
+
def axis_inches axis, tile, pixel
|
138
|
+
@offset[axis] + tile*@tile_interval[axis] + pixel*@scale[axis]
|
139
|
+
end
|
140
|
+
|
141
|
+
def x_inches tile, pixel
|
142
|
+
axis_inches 0, tile, pixel
|
143
|
+
end
|
144
|
+
|
145
|
+
def y_inches tile, pixel
|
146
|
+
axis_inches 1, tile, pixel
|
147
|
+
end
|
148
|
+
|
149
|
+
# return a complete tiled row of spans converted to inches
|
150
|
+
def tiled_row_spans y, forward=true
|
151
|
+
spans = @image.spans[y]
|
152
|
+
return spans if spans.empty?
|
153
|
+
tiled_spans = []
|
154
|
+
|
155
|
+
if forward
|
156
|
+
@config[:repeat][0].times do |tile|
|
157
|
+
spans.each do |span|
|
158
|
+
tiled_spans << [x_inches(tile,span[0]), x_inches(tile,span[1])]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
else
|
162
|
+
@config[:repeat][0].times.reverse_each do |tile|
|
163
|
+
spans.reverse_each do |span|
|
164
|
+
tiled_spans << [x_inches(tile,span[1]), x_inches(tile,span[0])]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
return tiled_spans
|
170
|
+
end
|
171
|
+
|
172
|
+
def build_tiled_rows
|
173
|
+
forward = false
|
174
|
+
@tiled_rows = @image.size[1].times.map {|y| tiled_row_spans y, (forward = !forward) }
|
175
|
+
end
|
176
|
+
|
177
|
+
# generate a unique id for this job
|
178
|
+
def job_hash
|
179
|
+
[@image,@config].hash
|
180
|
+
end
|
181
|
+
|
182
|
+
# render a complete tiled image to gcode and gmask streams
|
183
|
+
def render_tiled_image gcode, gmask
|
184
|
+
debug "rendering tiled image"
|
185
|
+
job_id = job_hash
|
186
|
+
hyst = -@scale[0]/2
|
187
|
+
gcode.comment "raster gcode for job #{job_id}"
|
188
|
+
gcode.comment "image: #{@image.filename} #{@image.size.inspect}"
|
189
|
+
gcode.comment "config: #{@config.inspect}"
|
190
|
+
|
191
|
+
gcode.preamble :feed => @config[:feed], :mask => true
|
192
|
+
gmask.preamble
|
193
|
+
|
194
|
+
@config[:repeat][1].times do |ytile|
|
195
|
+
debug "begin tile row #{ytile}"
|
196
|
+
@tiled_rows.each_with_index do |spans, ypix|
|
197
|
+
debug "pixel row #{ypix} is empty" if spans.empty?
|
198
|
+
unless spans.empty?
|
199
|
+
yinches = y_inches(ytile, ypix)
|
200
|
+
forward = spans[0][0] < spans[-1][1]
|
201
|
+
dir = forward ? 1 : -1
|
202
|
+
|
203
|
+
debug "pixel row #{ypix} at #{yinches} inches going #{forward ? 'forward' : 'backward'} with #{spans.size} spans"
|
204
|
+
|
205
|
+
gcode.g0 :x => spans[0][0] - dir*@config[:overshoot], :y => yinches
|
206
|
+
gcode.g1 :x => spans[-1][1] + dir*@config[:overshoot], :y => yinches
|
207
|
+
gmask.begin_row forward
|
208
|
+
spans.each {|span| gmask.span forward, span[0]+hyst, span[1]+hyst }
|
209
|
+
end # unless spans.empty?
|
210
|
+
end # @image.each_row
|
211
|
+
debug "end tile row #{ytile}"
|
212
|
+
end # @config[:repeat][i].times
|
213
|
+
|
214
|
+
gcode.epilogue
|
215
|
+
end # def render_tiled_image
|
216
|
+
|
217
|
+
# cut out the tile with bottom left at x,y
|
218
|
+
def render_cut gcode, x, y
|
219
|
+
radius = @config[:corner_radius]
|
220
|
+
left = x
|
221
|
+
bottom = y
|
222
|
+
right = x+@config[:tile_size][0]
|
223
|
+
top = y+@config[:tile_size][1]
|
224
|
+
|
225
|
+
gcode.instance_eval do
|
226
|
+
if radius && radius > 0
|
227
|
+
jog :x => left, :y => bottom+radius
|
228
|
+
move :x => left, :y => top-radius, :laser => true
|
229
|
+
turn_cw :x => left+radius, :y => top, :i => radius
|
230
|
+
move :x => right-radius, :y => top
|
231
|
+
turn_cw :x => right, :y => top-radius, :j => -radius
|
232
|
+
move :x => right, :y => bottom+radius
|
233
|
+
turn_cw :x => right-radius, :y => bottom, :i => -radius
|
234
|
+
move :x => left+radius, :y => bottom
|
235
|
+
turn_cw :x => left, :y => bottom+radius, :j => radius
|
236
|
+
nc :laser => false
|
237
|
+
else
|
238
|
+
jog :x => left, :y => bottom
|
239
|
+
move :x => left, :y => top, :laser => true
|
240
|
+
move :x => right, :y => top
|
241
|
+
move :x => right, :y => bottom
|
242
|
+
move :x => left, :y => bottom
|
243
|
+
nc :laser => false
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# render gcode to cut out the tiles
|
249
|
+
def render_all_cuts gcode
|
250
|
+
gcode.preamble :feed => @config[:cut_feed]
|
251
|
+
@config[:repeat][1].times do |ytile|
|
252
|
+
@config[:repeat][0].times do |xtile|
|
253
|
+
render_cut gcode, x_inches(xtile, 0), y_inches(ytile, 0)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
gcode.epilogue
|
257
|
+
end
|
258
|
+
|
259
|
+
def render_all gcode, gmask, cuts
|
260
|
+
render_tiled_image gcode, gmask
|
261
|
+
render_all_cuts cuts
|
262
|
+
end
|
263
|
+
|
264
|
+
def open_gcode_file &block
|
265
|
+
io = GcodeFile.open "#{@image.filename}.raster.ngc", "w", &block
|
266
|
+
end
|
267
|
+
|
268
|
+
def open_gmask_file &block
|
269
|
+
io = GmaskFile.open "#{@image.filename}.raster.gmask", "w", &block
|
270
|
+
end
|
271
|
+
|
272
|
+
def open_cut_file &block
|
273
|
+
io = GcodeFile.open "#{@image.filename}.cut.ngc", "w", &block
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_all_files
|
277
|
+
open_gcode_file do |gcode|
|
278
|
+
open_gmask_file do |gmask|
|
279
|
+
render_tiled_image gcode, gmask
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
open_cut_file do |cut|
|
284
|
+
render_all_cuts cut
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def config_to_yaml
|
289
|
+
@config.map {|k,v| "#{k}: #{v.inspect}\n" }.join
|
290
|
+
end
|
291
|
+
|
292
|
+
def debug msg
|
293
|
+
STDERR.puts msg if @debug
|
294
|
+
end
|
295
|
+
|
296
|
+
def initialize opts={}
|
297
|
+
self.config = DEFAULTS.dup
|
298
|
+
|
299
|
+
if opts[:config_file]
|
300
|
+
self.merge_config load_config_file opts[:config_file]
|
301
|
+
elsif opts[:default_config_file] && c = try_load_default_config_file
|
302
|
+
self.merge_config c
|
303
|
+
end
|
304
|
+
|
305
|
+
self.merge_config opts[:config] if opts[:config]
|
306
|
+
|
307
|
+
@debug = opts[:debug]
|
308
|
+
|
309
|
+
if opts[:image]
|
310
|
+
image = opts[:image]
|
311
|
+
elsif opts[:image_file]
|
312
|
+
load_image_file opts[:image_file]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
end # class Graster
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class Graster
|
2
|
+
class GcodeFile < File
|
3
|
+
def preamble opts
|
4
|
+
@laser = false
|
5
|
+
self << "M63 P0\nG61\nF#{opts[:feed] || 60}\n"
|
6
|
+
self << "M101\n" if opts[:mask]
|
7
|
+
self << "M3 S1\n"
|
8
|
+
end
|
9
|
+
|
10
|
+
def epilogue
|
11
|
+
self << "M63 P0\nM5\nM2\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
PRIORITY = [:g,:x,:y,:z,:w,:i,:j,:k,:m,:p,:s]
|
15
|
+
|
16
|
+
def nc codes
|
17
|
+
codes = codes.dup
|
18
|
+
|
19
|
+
if codes[:laser] == true && !@laser
|
20
|
+
@laser = true
|
21
|
+
codes.merge!(:m => 62, :p => 0)
|
22
|
+
elsif codes[:laser] == false && @laser
|
23
|
+
@laser = false
|
24
|
+
codes.merge!(:m => 63, :p => 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
codes.delete :laser
|
28
|
+
|
29
|
+
self << codes.sort {|(k1,v1),(k2,v2)|
|
30
|
+
PRIORITY.index(k1) <=> PRIORITY.index(k2)
|
31
|
+
}.map {|k,v|
|
32
|
+
if v.is_a? Integer
|
33
|
+
"#{k.to_s.upcase}#{v}"
|
34
|
+
elsif v.is_a? Float
|
35
|
+
"#{k.to_s.upcase}%0.3f" % v
|
36
|
+
else
|
37
|
+
k.to_s.upcase
|
38
|
+
end
|
39
|
+
}.join(' ') + "\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def g0 codes
|
43
|
+
nc({:g => 0}.merge codes)
|
44
|
+
end
|
45
|
+
alias_method :jog, :g0
|
46
|
+
|
47
|
+
def g1 codes
|
48
|
+
nc({:g => 1}.merge codes)
|
49
|
+
end
|
50
|
+
alias_method :move, :g1
|
51
|
+
|
52
|
+
def g2 codes
|
53
|
+
nc codes.merge(:g => 2)
|
54
|
+
end
|
55
|
+
alias_method :turn_cw, :g2
|
56
|
+
|
57
|
+
def g3 codes
|
58
|
+
nc codes.merge(:g => 3)
|
59
|
+
end
|
60
|
+
alias_method :turn_ccw, :g3
|
61
|
+
|
62
|
+
def comment txt
|
63
|
+
txt = txt.gsub(/\(\)/,'')
|
64
|
+
self << "(#{txt})\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
def puts *a
|
68
|
+
self.puts *a
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Graster
|
2
|
+
class GmaskFile < File
|
3
|
+
def preamble
|
4
|
+
self << "1 0 0 0\n"
|
5
|
+
end
|
6
|
+
|
7
|
+
def begin_row forward
|
8
|
+
@begin_row = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def span forward, x1, x2
|
12
|
+
if forward
|
13
|
+
self << "0 0 0 %0.3f\n" % x1 if @begin_row
|
14
|
+
self << "0 0 1 %0.3f\n" % x1
|
15
|
+
self << "0 1 1 %0.3f\n" % x2
|
16
|
+
else
|
17
|
+
self << "0 0 1 %0.3f\n" % x1 if @begin_row
|
18
|
+
self << "0 0 0 %0.3f\n" % x1
|
19
|
+
self << "0 1 0 %0.3f\n" % x2
|
20
|
+
end
|
21
|
+
@begin_row = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class Graster
|
2
|
+
class Image
|
3
|
+
PROPS = [:filename,:size,:pixels]
|
4
|
+
|
5
|
+
def initialize(props)
|
6
|
+
PROPS.each do |p|
|
7
|
+
raise "required image property :#{p} missing" unless props[p]
|
8
|
+
instance_variable_set "@#{p}", props[p]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
PROPS.each{|p| attr_reader p }
|
13
|
+
|
14
|
+
def self.from_file pathname
|
15
|
+
raise "file not found #{pathname}" unless File.exist? pathname
|
16
|
+
img = Magick::Image.read(pathname)
|
17
|
+
raise "bad image data in #{pathname}" unless img = img[0]
|
18
|
+
new :filename => File.basename(pathname),
|
19
|
+
:size => [img.columns,img.rows],
|
20
|
+
:pixels => img.export_pixels(0,0,img.columns,img.rows,"I")
|
21
|
+
end
|
22
|
+
|
23
|
+
# get pixel(s) from x,y coords
|
24
|
+
# 0,0 is bottom,left
|
25
|
+
# image[x,y] => pixel at x,y
|
26
|
+
# image[y] => row at y
|
27
|
+
def [] y, x=nil
|
28
|
+
if x
|
29
|
+
@pixels[(@size[1]-y)*@size[0]+x]
|
30
|
+
else
|
31
|
+
@pixels[(@size[1]-y)*@size[0],@size[0]]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def each_row &block
|
36
|
+
@pixels.chars.each_slice(@size[0]).each_with_index &block
|
37
|
+
end
|
38
|
+
|
39
|
+
# "encode" a float 0..1 to a pixel
|
40
|
+
def self.f_to_pix f
|
41
|
+
(f*65535).round
|
42
|
+
end
|
43
|
+
|
44
|
+
# "decode" an encoded pixel to a float 0..1
|
45
|
+
def self.pix_to_f pix
|
46
|
+
pix/65535.0
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# convert bitmap data to spans (or runs) of contiguous pixels
|
51
|
+
# also invert the Y axis
|
52
|
+
def build_spans on_range
|
53
|
+
# TODO: rewrite in terms of each_row
|
54
|
+
@spans = Array.new @size[1]
|
55
|
+
|
56
|
+
@size[1].times do |y|
|
57
|
+
spans = []
|
58
|
+
left = (@size[1]-y-1)*@size[0]
|
59
|
+
start = nil
|
60
|
+
|
61
|
+
@size[0].times do |x|
|
62
|
+
d = on_range.include?(@pixels[left+x])
|
63
|
+
|
64
|
+
if !start && d
|
65
|
+
start = x
|
66
|
+
elsif start && !d
|
67
|
+
spans << [start, x]
|
68
|
+
start = nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
spans << [start, @size[0]] if start
|
73
|
+
@spans[y] = spans
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :spans
|
78
|
+
|
79
|
+
def hash
|
80
|
+
[@pixels,@width,@height].hash
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class Graster
|
4
|
+
class Runner
|
5
|
+
|
6
|
+
attr_reader :options, :args, :opts
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@args = args
|
10
|
+
@options = { :default_config_file => true }
|
11
|
+
@opts = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: graster [options] image"
|
13
|
+
|
14
|
+
opts.on "-c", "--config FILE", "Use specified configuration file.",
|
15
|
+
"The default is ./graster.yml" do |c|
|
16
|
+
@options[:config_file] = c
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on "-g", "--generate", "generate a configuration file with","defaults" do
|
20
|
+
@options[:generate_config] = true
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on "-d", "--debug", "Dump useless debug info" do
|
24
|
+
@options[:debug] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
Graster::OPTIONS.each do |key,info|
|
28
|
+
type,sym,*desc = info
|
29
|
+
|
30
|
+
if type.is_a? Array
|
31
|
+
cast = type[0].name.intern
|
32
|
+
type = Array
|
33
|
+
else
|
34
|
+
cast = type.name.intern
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on "--#{key.to_s.gsub /_/, '-'} #{sym}", type, *desc do |x|
|
38
|
+
@options[:config] ||= {}
|
39
|
+
if type == Array
|
40
|
+
x = x.map {|s| Kernel.send(cast,s) }
|
41
|
+
else
|
42
|
+
x = Kernel.send(cast,x)
|
43
|
+
end
|
44
|
+
|
45
|
+
@options[:config][key] = x
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@opts.parse!(args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def start!
|
54
|
+
if @options[:generate_config]
|
55
|
+
print Graster.new(@options).config_to_yaml
|
56
|
+
else
|
57
|
+
unless options[:image_file] = args.shift
|
58
|
+
puts @opts
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
Graster.new(options).generate_all_files
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jedediah Smith
|
8
|
+
- Joshua Hull
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-10-20 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: RMagick
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
version:
|
26
|
+
description: G Raster!
|
27
|
+
email: joshbuddy@gmail.com
|
28
|
+
executables:
|
29
|
+
- graster
|
30
|
+
- gtile
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- Rakefile
|
37
|
+
- VERSION
|
38
|
+
- bin/graster
|
39
|
+
- bin/gtile
|
40
|
+
- lib/graster.rb
|
41
|
+
- lib/graster/gcode_file.rb
|
42
|
+
- lib/graster/gmask_file.rb
|
43
|
+
- lib/graster/image.rb
|
44
|
+
- lib/graster/runner.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/joshbuddy/graster
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: graster
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: G Raster!
|
73
|
+
test_files: []
|
74
|
+
|