--- a/printrun-src/printrun/laser.py Sat Jun 04 10:01:58 2016 +0200 +++ b/printrun-src/printrun/laser.py Sat Jun 04 12:41:32 2016 +0200 @@ -6,32 +6,14 @@ """ LASERCUT SETTINGS -TODO: move to printrun settings +Will be overridden from pronterface settings """ -ENGRAVE_SPEED = 10 * 60 # mm/min -# 30mm/min works for wood (regulate the output power to something between 10-30%) -# 30mm/min for black anodized aluminum to get a light engraving @ 100% power -# 10mm/min for black anodized aluminum to get more "silver" @ 100% power - -TRAVEL_SPEED = 120 * 60 E_FACTOR = 0.5 -# BITMAP: -DPI = 300 -GREY_THRESHOLD = 0 -CHANGE_DIRECTION = True -INVERT_CUT = True +from PIL import Image +import sys +import math -""" -STATIC DEFINITIONS -DO NOT CHANGE WORLD's RULES! -""" -INCH = 25.4 # mm -MM_PIXEL = round(INCH / DPI, 4) -STEPS_PIXEL = MM_PIXEL * 80 # mine is 80 steps/mm on XY - -# FOR HPGL: -SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units # GENERAL HEADER AND FOOTER GCODE GCODE_HEAD = """ @@ -48,27 +30,39 @@ M209 S0 ; disable firmware retraction, we dont want to burn holes... M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement! -G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED -G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED - -""" % (TRAVEL_SPEED, ENGRAVE_SPEED) +""" GCODE_FOOT = """G0 X0 Y0 F%.4f M400 ; Wait for all moves to finish M571 S0 E0 M42 P28 S0 ; Force laser off! M501 ; undo all settings made -""" % (TRAVEL_SPEED) +""" % (100*60) -from PIL import Image -import sys -# Imports for SVG -import xml.etree.ElementTree as ET -import math -from svg2gcode import shapes as shapes_pkg -from svg2gcode.shapes import point_generator +class LasercutterSettings: + """ + Default settings object + """ + def __init__(self): + self.lc_engrave_speed = 10 + # 30mm/min works for wood (regulate the output power to something between 10-30%) + # 30mm/min for black anodized aluminum to get a light engraving @ 100% power + # 10mm/min for black anodized aluminum to get more "silver" @ 100% power + self.lc_travel_speed = 120 + # BITMAP: + self.lc_bitmap_speed_factor = 1.0 + self.lc_dpi = 300 + self.lc_grey_threshold = 0 + self.lc_change_dir = True + self.lc_invert_cut = True + + # HPGL: + self.lc_hpgl_speed_factor = 1.0 + + # SVG: + self.lc_svg_speed_factor = 1.0 class Lasercutter: """ @@ -78,24 +72,32 @@ def __init__(self, pronterwindow = None): if pronterwindow: self.pronterwindow = pronterwindow + self.settings = pronterwindow.settings #self.log = pronterwindow.log self.log = self.log_print self.pronterwindow.clear_log(None) else: self.pronterwindow = None + self.settings = LasercutterSettings() self.log = lambda : None - self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (DPI, MM_PIXEL)) - if STEPS_PIXEL <= 5: - self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % STEPS_PIXEL) + + # STATIC DEFINITIONS, DO NOT CHANGE WORLD's RULES! + self.INCH = 25.4 # mm + self.MM_PIXEL = round(self.INCH / self.settings.lc_dpi, 4) + self.STEPS_PIXEL = self.MM_PIXEL * 80 # mine is 80 steps/mm on XY + + self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (self.settings.lc_dpi, self.MM_PIXEL)) + if self.STEPS_PIXEL <= 5: + self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % self.STEPS_PIXEL) self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % ( - TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) ) + self.settings.lc_travel_speed, self.settings.lc_engrave_speed) ) self.log("") def log_print(self, msg): print(msg) - def pixel2bit(self, pixel, threshold=128): + def pixel2bit(self, pixel): """Convert the pixel value to a bit.""" # some really weird stuff here ;-P @@ -105,14 +107,14 @@ if isinstance(pixel, tuple): #rgb pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 - threshold = 128 - if pixel > threshold: + if pixel > self.settings.lc_grey_threshold: return 1 else: return 0 # color palette - if pixel <= threshold: + # TODO: get the grey value of the palette index instead of using pixel which is the palette index? + if pixel <= self.settings.lc_grey_threshold: return 1 else: return 0 @@ -140,12 +142,15 @@ fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % ( - im.size[0], im.size[1], DPI, im.size[0]*MM_PIXEL, im.size[1]*MM_PIXEL) ) + im.size[0], im.size[1], self.settings.lc_dpi, im.size[0] * self.MM_PIXEL, im.size[1] * self.MM_PIXEL) ) - INVERT_Y = MM_PIXEL * (im.size[1] -1) * (-1) + INVERT_Y = self.MM_PIXEL * (im.size[1] -1) * (-1) DIR = 1 + travel_speed = self.settings.lc_travel_speed * 60 + engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_bitmap_speed_factor for X in range(im.size[0]): - fo.write("; X=%d printing row: direction %i\n" % (X, DIR)) + # TODO: Skip empty rows!!! + fo.write("M400 ; X=%d printing row: direction %i\n" % (X, DIR)) fo.write("G92 E0\n") E = 0 last_bit = 1 # we engrave on black pixel = 0 @@ -158,11 +163,11 @@ range_stop = -1 for Y in range(range_start, range_stop, DIR): - YMM = abs((Y * MM_PIXEL) + INVERT_Y) - XMM = X * MM_PIXEL + YMM = abs((Y * self.MM_PIXEL) + INVERT_Y) + XMM = X * self.MM_PIXEL #print "X %d Y %d" % (X, Y) - bit = self.pixel2bit(pix[X, Y], GREY_THRESHOLD) - if INVERT_CUT: + bit = self.pixel2bit(pix[X, Y]) + if self.settings.lc_invert_cut: if bit == 0: bit = 1 else: @@ -177,25 +182,25 @@ if (Y == (im.size[1] - 1)) or (Y == 0): # draw line if DIR > 0: - E = E + MM_PIXEL * (Y - START_Y) + E = E + self.MM_PIXEL * (Y - START_Y) else: - E = E + MM_PIXEL * (START_Y - Y) - fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) + E = E + self.MM_PIXEL * (START_Y - Y) + fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed)) else: # bit value has changed! if bit == 0: # jump to start of line to write START_Y = Y - fo.write("G0 X%.4f Y%.4f\n" % (XMM, YMM)) + fo.write("G0 X%.4f Y%.4f F%.4f\n" % (XMM, YMM, travel_speed)) else: # end of line to write if DIR > 0: - E = E + (MM_PIXEL * (Y - START_Y)) + E = E + (self.MM_PIXEL * (Y - START_Y)) else: - E = E + (MM_PIXEL * (START_Y - Y)) - fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR)) + E = E + (self.MM_PIXEL * (START_Y - Y)) + fo.write("G1 X%.4f Y%.4f E%.4f F%.4f\n" % (XMM, YMM, E * E_FACTOR, engrave_speed)) last_bit = bit - if CHANGE_DIRECTION: + if self.settings.lc_change_dir: DIR = DIR * (-1) # change y direction on every X fo.write(GCODE_FOOT) @@ -206,6 +211,8 @@ self.pronterwindow.load_gcode_async(filename + '.g') def hpgl2gcode(self, filename): + # FOR HPGL: + SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units OFFSET_X = 0.0 OFFSET_Y = 0.0 @@ -221,6 +228,10 @@ last_coord = [0.0,0.0] last_cmd = "" + travel_speed = self.settings.lc_travel_speed * 60 + engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_hpgl_speed_factor + + for line in fi.readlines(): for action in line.split(";"): action = action.strip() @@ -242,9 +253,9 @@ if LASER_STATE: EN = " E%.4f F%.4f" % ( E_FACTOR * math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]), - ENGRAVE_SPEED * 0.5 ) # 1/2 engraving speed + engrave_speed) else: - EN = " F%.4f" % TRAVEL_SPEED + EN = " F%.4f" % travel_speed fo.write("G%d X%.4f Y%.4f%s\n" % ( LASER_STATE, coord[0], coord[1], EN) ) @@ -265,22 +276,15 @@ self.log("") self.pronterwindow.load_gcode_async(filename + '.g') - def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2): - self.log("Generating paths from SVG...") - - shape_preamble = "G92 E0\n" - shape_postamble = "" + # Imports for SVG + import xml.etree.ElementTree as ET + from svg2gcode import shapes as shapes_pkg + from svg2gcode.shapes import point_generator - """ - Used to control the smoothness/sharpness of the curves. - Smaller the value greater the sharpness. Make sure the - value is greater than 0.1 - """ + self.log("Generating paths from SVG, alternative lib (outlines only)...") if smoothness < 0.1: smoothness = 0.1 - svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) - tree = ET.parse(filename) root = tree.getroot() @@ -295,8 +299,8 @@ self.log("Unable to get width and height for the svg!") return False - width = float(width.replace("px", "")) - height = float(height.replace("px", "")) + width = float(width.replace("px", "").replace("pt", "")) + height = float(height.replace("px", "").replace("pt", "")) scale_x = bed_max_x / max(width, height) scale_y = bed_max_y / max(width, height) @@ -306,6 +310,9 @@ fo = open(filename + ".g", "w") fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) + travel_speed = self.settings.lc_travel_speed * 60 + engrave_speed = self.settings.lc_engrave_speed * 60 * self.settings.lc_svg_speed_factor + for elem in root.iter(): try: _, tag_suffix = elem.tag.split('}') @@ -319,11 +326,11 @@ m = shape_obj.transformation_matrix() if d: - fo.write("M400 ; wait for moves finish, then printing shape: %s\n" % (tag_suffix)) + fo.write("M400 ; start %s\n" % (tag_suffix)) + fo.write("G92 E0\n") E = 0 xo = 0 yo = 0 - fo.write(shape_preamble) p = point_generator(d, m, smoothness) start = True for x,y,pen in p: @@ -335,7 +342,7 @@ if not pen: start = True if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y: if start: - fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, TRAVEL_SPEED)) + fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, travel_speed)) start = False xo = xs yo = ys @@ -346,17 +353,15 @@ xo = xs yo = ys E = E + (e_distance) - fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, ENGRAVE_SPEED)) + fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, engrave_speed)) else: self.log("Position outside print dimension: %d, %d" % (xs, ys)) if shape_obj.xml_node.get('fill'): # Close the polygon e_distance = math.hypot(object_xs - xo, object_ys - yo) E = E + (e_distance) - 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)) - print "connecting polycommon path end to start" - - fo.write(shape_postamble) + 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)) + print "connecting filled path end to start" fo.write(GCODE_FOOT) fo.close()