graster 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|