4 Intended to use standalone or implemented in Pronterface/Printrun |
4 Intended to use standalone or implemented in Pronterface/Printrun |
5 """ |
5 """ |
6 |
6 |
7 """ |
7 """ |
8 LASERCUT SETTINGS |
8 LASERCUT SETTINGS |
9 TODO: move to printrun settings |
9 Will be overridden from pronterface settings |
10 """ |
10 """ |
11 ENGRAVE_SPEED = 10 * 60 # mm/min |
|
12 # 30mm/min works for wood (regulate the output power to something between 10-30%) |
|
13 # 30mm/min for black anodized aluminum to get a light engraving @ 100% power |
|
14 # 10mm/min for black anodized aluminum to get more "silver" @ 100% power |
|
15 |
|
16 TRAVEL_SPEED = 120 * 60 |
|
17 E_FACTOR = 0.5 |
11 E_FACTOR = 0.5 |
18 |
12 |
19 # BITMAP: |
13 from PIL import Image |
20 DPI = 300 |
14 import sys |
21 GREY_THRESHOLD = 0 |
15 import math |
22 CHANGE_DIRECTION = True |
16 |
23 INVERT_CUT = True |
|
24 |
|
25 """ |
|
26 STATIC DEFINITIONS |
|
27 DO NOT CHANGE WORLD's RULES! |
|
28 """ |
|
29 INCH = 25.4 # mm |
|
30 MM_PIXEL = round(INCH / DPI, 4) |
|
31 STEPS_PIXEL = MM_PIXEL * 80 # mine is 80 steps/mm on XY |
|
32 |
|
33 # FOR HPGL: |
|
34 SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units |
|
35 |
17 |
36 # GENERAL HEADER AND FOOTER GCODE |
18 # GENERAL HEADER AND FOOTER GCODE |
37 GCODE_HEAD = """ |
19 GCODE_HEAD = """ |
38 ; GCode generated by laser.py pronterface library (marlin code flavour) |
20 ; GCode generated by laser.py pronterface library (marlin code flavour) |
39 ; 2015/2016 by NeoSoft - Malte Bayer |
21 ; 2015/2016 by NeoSoft - Malte Bayer |
46 M201 X1000 Y1000 E1000 ; Set acceleration |
28 M201 X1000 Y1000 E1000 ; Set acceleration |
47 M203 X1000 Y1000 Z4 E1000 ; Set max feedrate |
29 M203 X1000 Y1000 Z4 E1000 ; Set max feedrate |
48 M209 S0 ; disable firmware retraction, we dont want to burn holes... |
30 M209 S0 ; disable firmware retraction, we dont want to burn holes... |
49 M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod |
31 M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod |
50 M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! |
32 M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! |
51 G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED |
33 """ |
52 G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED |
|
53 |
|
54 """ % (TRAVEL_SPEED, ENGRAVE_SPEED) |
|
55 |
34 |
56 GCODE_FOOT = """G0 X0 Y0 F%.4f |
35 GCODE_FOOT = """G0 X0 Y0 F%.4f |
57 M400 ; Wait for all moves to finish |
36 M400 ; Wait for all moves to finish |
58 M571 S0 E0 |
37 M571 S0 E0 |
59 M42 P28 S0 ; Force laser off! |
38 M42 P28 S0 ; Force laser off! |
60 M501 ; undo all settings made |
39 M501 ; undo all settings made |
61 """ % (TRAVEL_SPEED) |
40 """ % (100*60) |
62 |
41 |
63 from PIL import Image |
42 |
64 import sys |
43 class LasercutterSettings: |
65 |
44 """ |
66 # Imports for SVG |
45 Default settings object |
67 import xml.etree.ElementTree as ET |
46 """ |
68 import math |
47 def __init__(self): |
69 from svg2gcode import shapes as shapes_pkg |
48 self.lc_engrave_speed = 10 |
70 from svg2gcode.shapes import point_generator |
49 # 30mm/min works for wood (regulate the output power to something between 10-30%) |
71 |
50 # 30mm/min for black anodized aluminum to get a light engraving @ 100% power |
|
51 # 10mm/min for black anodized aluminum to get more "silver" @ 100% power |
|
52 self.lc_travel_speed = 120 |
|
53 |
|
54 # BITMAP: |
|
55 self.lc_bitmap_speed_factor = 1.0 |
|
56 self.lc_dpi = 300 |
|
57 self.lc_grey_threshold = 0 |
|
58 self.lc_change_dir = True |
|
59 self.lc_invert_cut = True |
|
60 |
|
61 # HPGL: |
|
62 self.lc_hpgl_speed_factor = 1.0 |
|
63 |
|
64 # SVG: |
|
65 self.lc_svg_speed_factor = 1.0 |
72 |
66 |
73 class Lasercutter: |
67 class Lasercutter: |
74 """ |
68 """ |
75 Lasercutter methods |
69 Lasercutter methods |
76 parameters: log = logger function (fuction has to accept a string) |
70 parameters: log = logger function (fuction has to accept a string) |
77 """ |
71 """ |
78 def __init__(self, pronterwindow = None): |
72 def __init__(self, pronterwindow = None): |
79 if pronterwindow: |
73 if pronterwindow: |
80 self.pronterwindow = pronterwindow |
74 self.pronterwindow = pronterwindow |
|
75 self.settings = pronterwindow.settings |
81 #self.log = pronterwindow.log |
76 #self.log = pronterwindow.log |
82 self.log = self.log_print |
77 self.log = self.log_print |
83 self.pronterwindow.clear_log(None) |
78 self.pronterwindow.clear_log(None) |
84 else: |
79 else: |
85 self.pronterwindow = None |
80 self.pronterwindow = None |
|
81 self.settings = LasercutterSettings() |
86 self.log = lambda : None |
82 self.log = lambda : None |
87 self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (DPI, MM_PIXEL)) |
83 |
88 if STEPS_PIXEL <= 5: |
84 # STATIC DEFINITIONS, DO NOT CHANGE WORLD's RULES! |
89 self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % STEPS_PIXEL) |
85 self.INCH = 25.4 # mm |
|
86 self.MM_PIXEL = round(self.INCH / self.settings.lc_dpi, 4) |
|
87 self.STEPS_PIXEL = self.MM_PIXEL * 80 # mine is 80 steps/mm on XY |
|
88 |
|
89 self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (self.settings.lc_dpi, self.MM_PIXEL)) |
|
90 if self.STEPS_PIXEL <= 5: |
|
91 self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % self.STEPS_PIXEL) |
90 self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( |
92 self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( |
91 TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) ) |
93 self.settings.lc_travel_speed, self.settings.lc_engrave_speed) ) |
92 self.log("") |
94 self.log("") |
93 |
95 |
94 def log_print(self, msg): |
96 def log_print(self, msg): |
95 print(msg) |
97 print(msg) |
96 |
98 |
97 |
99 |
98 def pixel2bit(self, pixel, threshold=128): |
100 def pixel2bit(self, pixel): |
99 """Convert the pixel value to a bit.""" |
101 """Convert the pixel value to a bit.""" |
100 # some really weird stuff here ;-P |
102 # some really weird stuff here ;-P |
101 |
103 |
102 # RGB to greyscale |
104 # RGB to greyscale |
103 #print pixel |
105 #print pixel |
104 #print type(pixel) |
106 #print type(pixel) |
105 if isinstance(pixel, tuple): |
107 if isinstance(pixel, tuple): |
106 #rgb |
108 #rgb |
107 pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 |
109 pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 |
108 threshold = 128 |
110 if pixel > self.settings.lc_grey_threshold: |
109 if pixel > threshold: |
|
110 return 1 |
111 return 1 |
111 else: |
112 else: |
112 return 0 |
113 return 0 |
113 |
114 |
114 # color palette |
115 # color palette |
115 if pixel <= threshold: |
116 # TODO: get the grey value of the palette index instead of using pixel which is the palette index? |
|
117 if pixel <= self.settings.lc_grey_threshold: |
116 return 1 |
118 return 1 |
117 else: |
119 else: |
118 return 0 |
120 return 0 |
119 |
121 |
120 def image2gcode(self, filename): |
122 def image2gcode(self, filename): |
138 |
140 |
139 fo = open(filename + ".g", "w") |
141 fo = open(filename + ".g", "w") |
140 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) |
142 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) |
141 |
143 |
142 fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % ( |
144 fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % ( |
143 im.size[0], im.size[1], DPI, im.size[0]*MM_PIXEL, im.size[1]*MM_PIXEL) ) |
145 im.size[0], im.size[1], self.settings.lc_dpi, im.size[0] * self.MM_PIXEL, im.size[1] * self.MM_PIXEL) ) |
144 |
146 |
145 INVERT_Y = MM_PIXEL * (im.size[1] -1) * (-1) |
147 INVERT_Y = self.MM_PIXEL * (im.size[1] -1) * (-1) |
146 DIR = 1 |
148 DIR = 1 |
|
149 travel_speed = self.settings.lc_travel_speed * 60 |
|
150 engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_bitmap_speed_factor |
147 for X in range(im.size[0]): |
151 for X in range(im.size[0]): |
148 fo.write("; X=%d printing row: direction %i\n" % (X, DIR)) |
152 # TODO: Skip empty rows!!! |
|
153 fo.write("M400 ; X=%d printing row: direction %i\n" % (X, DIR)) |
149 fo.write("G92 E0\n") |
154 fo.write("G92 E0\n") |
150 E = 0 |
155 E = 0 |
151 last_bit = 1 # we engrave on black pixel = 0 |
156 last_bit = 1 # we engrave on black pixel = 0 |
152 START_Y = 0 |
157 START_Y = 0 |
153 if DIR > 0: |
158 if DIR > 0: |
175 # are we at the end of Y range? |
180 # are we at the end of Y range? |
176 #print Y |
181 #print Y |
177 if (Y == (im.size[1] - 1)) or (Y == 0): |
182 if (Y == (im.size[1] - 1)) or (Y == 0): |
178 # draw line |
183 # draw line |
179 if DIR > 0: |
184 if DIR > 0: |
180 E = E + MM_PIXEL * (Y - START_Y) |
185 E = E + self.MM_PIXEL * (Y - START_Y) |
181 else: |
186 else: |
182 E = E + MM_PIXEL * (START_Y - Y) |
187 E = E + self.MM_PIXEL * (START_Y - Y) |
183 fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) |
188 fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed)) |
184 else: |
189 else: |
185 # bit value has changed! |
190 # bit value has changed! |
186 if bit == 0: |
191 if bit == 0: |
187 # jump to start of line to write |
192 # jump to start of line to write |
188 START_Y = Y |
193 START_Y = Y |
189 fo.write("G0 X%.4f Y%.4f\n" % (XMM, YMM)) |
194 fo.write("G0 X%.4f Y%.4f F%.4f\n" % (XMM, YMM, travel_speed)) |
190 else: |
195 else: |
191 # end of line to write |
196 # end of line to write |
192 if DIR > 0: |
197 if DIR > 0: |
193 E = E + (MM_PIXEL * (Y - START_Y)) |
198 E = E + (self.MM_PIXEL * (Y - START_Y)) |
194 else: |
199 else: |
195 E = E + (MM_PIXEL * (START_Y - Y)) |
200 E = E + (self.MM_PIXEL * (START_Y - Y)) |
196 fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) |
201 fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed)) |
197 last_bit = bit |
202 last_bit = bit |
198 if CHANGE_DIRECTION: |
203 if self.settings.lc_change_dir: |
199 DIR = DIR * (-1) # change y direction on every X |
204 DIR = DIR * (-1) # change y direction on every X |
200 |
205 |
201 fo.write(GCODE_FOOT) |
206 fo.write(GCODE_FOOT) |
202 fo.close() |
207 fo.close() |
203 |
208 |
204 if self.pronterwindow: |
209 if self.pronterwindow: |
205 self.log("") |
210 self.log("") |
206 self.pronterwindow.load_gcode_async(filename + '.g') |
211 self.pronterwindow.load_gcode_async(filename + '.g') |
207 |
212 |
208 def hpgl2gcode(self, filename): |
213 def hpgl2gcode(self, filename): |
|
214 # FOR HPGL: |
|
215 SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units |
209 OFFSET_X = 0.0 |
216 OFFSET_X = 0.0 |
210 OFFSET_Y = 0.0 |
217 OFFSET_Y = 0.0 |
211 |
218 |
212 self.log("Converting HPGL plot for lasercut:") |
219 self.log("Converting HPGL plot for lasercut:") |
213 self.log("File: %s" % filename) |
220 self.log("File: %s" % filename) |
263 |
274 |
264 if self.pronterwindow: |
275 if self.pronterwindow: |
265 self.log("") |
276 self.log("") |
266 self.pronterwindow.load_gcode_async(filename + '.g') |
277 self.pronterwindow.load_gcode_async(filename + '.g') |
267 |
278 |
268 |
|
269 def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2): |
279 def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2): |
270 self.log("Generating paths from SVG...") |
280 # Imports for SVG |
271 |
281 import xml.etree.ElementTree as ET |
272 shape_preamble = "G92 E0\n" |
282 from svg2gcode import shapes as shapes_pkg |
273 shape_postamble = "" |
283 from svg2gcode.shapes import point_generator |
274 |
284 |
275 """ |
285 self.log("Generating paths from SVG, alternative lib (outlines only)...") |
276 Used to control the smoothness/sharpness of the curves. |
|
277 Smaller the value greater the sharpness. Make sure the |
|
278 value is greater than 0.1 |
|
279 """ |
|
280 if smoothness < 0.1: smoothness = 0.1 |
286 if smoothness < 0.1: smoothness = 0.1 |
281 |
|
282 svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) |
287 svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) |
283 |
|
284 tree = ET.parse(filename) |
288 tree = ET.parse(filename) |
285 root = tree.getroot() |
289 root = tree.getroot() |
286 |
290 |
287 width = root.get('width') |
291 width = root.get('width') |
288 height = root.get('height') |
292 height = root.get('height') |
293 |
297 |
294 if width == None or height == None: |
298 if width == None or height == None: |
295 self.log("Unable to get width and height for the svg!") |
299 self.log("Unable to get width and height for the svg!") |
296 return False |
300 return False |
297 |
301 |
298 width = float(width.replace("px", "")) |
302 width = float(width.replace("px", "").replace("pt", "")) |
299 height = float(height.replace("px", "")) |
303 height = float(height.replace("px", "").replace("pt", "")) |
300 |
304 |
301 scale_x = bed_max_x / max(width, height) |
305 scale_x = bed_max_x / max(width, height) |
302 scale_y = bed_max_y / max(width, height) |
306 scale_y = bed_max_y / max(width, height) |
303 |
307 |
304 self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y)) |
308 self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y)) |
305 |
309 |
306 fo = open(filename + ".g", "w") |
310 fo = open(filename + ".g", "w") |
307 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) |
311 fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) |
|
312 |
|
313 travel_speed = self.settings.lc_travel_speed * 60 |
|
314 engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_svg_speed_factor |
308 |
315 |
309 for elem in root.iter(): |
316 for elem in root.iter(): |
310 try: |
317 try: |
311 _, tag_suffix = elem.tag.split('}') |
318 _, tag_suffix = elem.tag.split('}') |
312 except ValueError: |
319 except ValueError: |
333 if xo == xs and yo == ys: continue |
340 if xo == xs and yo == ys: continue |
334 |
341 |
335 if not pen: start = True |
342 if not pen: start = True |
336 if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y: |
343 if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y: |
337 if start: |
344 if start: |
338 fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, TRAVEL_SPEED)) |
345 fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, travel_speed)) |
339 start = False |
346 start = False |
340 xo = xs |
347 xo = xs |
341 yo = ys |
348 yo = ys |
342 object_xs = xs |
349 object_xs = xs |
343 object_ys = ys |
350 object_ys = ys |
344 else: |
351 else: |
345 e_distance = math.hypot(xs - xo, ys - yo) |
352 e_distance = math.hypot(xs - xo, ys - yo) |
346 xo = xs |
353 xo = xs |
347 yo = ys |
354 yo = ys |
348 E = E + (e_distance) |
355 E = E + (e_distance) |
349 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, ENGRAVE_SPEED)) |
356 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, engrave_speed)) |
350 else: |
357 else: |
351 self.log("Position outside print dimension: %d, %d" % (xs, ys)) |
358 self.log("Position outside print dimension: %d, %d" % (xs, ys)) |
352 if shape_obj.xml_node.get('fill'): |
359 if shape_obj.xml_node.get('fill'): |
353 # Close the polygon |
360 # Close the polygon |
354 e_distance = math.hypot(object_xs - xo, object_ys - yo) |
361 e_distance = math.hypot(object_xs - xo, object_ys - yo) |
355 E = E + (e_distance) |
362 E = E + (e_distance) |
356 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f ; Close the object polygon\n" % (object_xs, object_ys, E * E_FACTOR, ENGRAVE_SPEED)) |
363 fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f ; Close the object polygon\n" % (object_xs, object_ys, E * E_FACTOR, engrave_speed)) |
357 print "connecting polycommon path end to start" |
364 print "connecting filled path end to start" |
358 |
|
359 fo.write(shape_postamble) |
|
360 |
365 |
361 fo.write(GCODE_FOOT) |
366 fo.write(GCODE_FOOT) |
362 fo.close() |
367 fo.close() |
363 |
368 |
364 if self.pronterwindow: |
369 if self.pronterwindow: |