Tue, 19 Jan 2021 20:25:47 +0100
NeoCube laser cutting improvements
""" Lasercutter library 2015-2019 by NeoSoft, Malte Di Donato Intended to use standalone or implemented in Pronterface/Printrun """ """ LASERCUT SETTINGS Will be overridden from pronterface settings """ E_FACTOR = 0.5 from PIL import Image import sys import math # GENERAL HEADER AND FOOTER GCODE GCODE_HEAD = """ ; GCode generated by laser.py pronterface library (marlin code flavour) ; 2015-2019 by NeoSoft - Malte Di Donato G21 ; Metric ; We assume Z is in focus height and laser head is focus at bottom left of image! G92 X0 Y0 E0; set zero position - new origin G90 ; absolute positioning M201 X1000 Y1000 ; Set acceleration M203 X1000 Y1000 ; Set max feedrate M209 S0 ; disable firmware retraction, we dont want to burn holes... ;M85 S0 ; Disable idle hold timeout (BUG!) M84 ; enable motors """ GCODE_FOOT = """ M400 ; Wait for all moves to finish M5 ; Force laser off! G0 X0 Y0 F%.4f ; Move back to origin ; M501 ; undo all settings made """ % (100*60) GCODE_HEAD_MELZI = """ ; GCode generated by laser.py pronterface library (marlin code flavour) ; 2015-2019 by NeoSoft - Malte Di Donato G21 ; Metric ; We assume Z is in focus height and laser head is focus at bottom left of image! G92 X0 Y0 E0; set zero position - new origin G90 ; absolute positioning M82 ; Set extruder (laser) to absolute positioning M201 X1000 Y1000 E1000 ; Set acceleration M203 X1000 Y1000 Z4 E1000 ; Set max feedrate 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! ;M85 S0 ; Disable idle hold timeout (BUG!) M84 ; enable motors """ GCODE_FOOT_MELZI = """ M400 ; Wait for all moves to finish M42 P28 S0 ; Force laser off! ;M85 S30 ; re-enable idle hold timeout (BUG!) G0 X0 Y0 F%.4f ; Move back to origin M571 S0 E0 ; disable extruder firmware hack ; M501 ; undo all settings made """ % (100*60) class LasercutterSettings: """ Default settings object """ def __init__(self): self.lc_engrave_speed = 10 # 30mm/sec works for wood (regulate the output power to something between 10-30%) # 30mm/sec for black anodized aluminum to get a light engraving @ 100% power # 10mm/sec for black anodized aluminum to get maximum possible engraving! @ 100% power self.lc_travel_speed = 120 # insert config option to enable the Melzi Marlin FW Hack (M571) self.lc_melzi_hack = False # 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 self.lc_svg_smoothness = 0.2 self.lc_svg_width = 50 self.lc_svg_height = 50 self.lc_svg_scalemode = "scale" class Lasercutter: """ Lasercutter methods parameters: log = logger function (fuction has to accept a string) """ 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 # 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" % ( self.settings.lc_travel_speed, self.settings.lc_engrave_speed) ) self.log("") def log_print(self, msg): print(msg) def pixel2bit(self, pixel): """Convert the pixel value to a bit.""" # some really weird stuff here ;-P # RGB to greyscale #print pixel #print type(pixel) if isinstance(pixel, tuple): #rgb pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140 if pixel > self.settings.lc_grey_threshold: return 1 else: return 0 # color palette # 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 def image2gcode(self, filename): """ Open a image file and get the basic information about it. Then convert it to gcode (replacing the existing gcode buffer contents) """ try: im = Image.open(filename) except: self.log("Unable to open %s" % filename) return False self.log("Converting Image for lasercut:") self.log("File: %s" % filename) self.log("format: %s, mode: %s" % (im.format, im.mode)) width,height = im.size self.log("size: %d x %d pixels" % im.size) pix = im.load() fo = open(filename + ".g", "w") if self.settings.lc_melzi_hack: fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD_MELZI)) else: 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], self.settings.lc_dpi, im.size[0] * self.MM_PIXEL, im.size[1] * self.MM_PIXEL) ) 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]): gcode_col = "" first_ymm = None first_xmm = None E = 0 last_bit = 1 # we engrave on black pixel = 0 START_Y = 0 if DIR > 0: range_start = 0 range_stop = im.size[1] else: range_start = im.size[1] -1 range_stop = -1 for Y in range(range_start, range_stop, DIR): 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]) if self.settings.lc_invert_cut: if bit == 0: bit = 1 else: bit = 0 if last_bit == bit: if bit == 1: # nothing to do, continue else: # are we at the end of Y range? #print Y if (Y == (im.size[1] - 1)) or (Y == 0): # draw line if DIR > 0: E = E + self.MM_PIXEL * (Y - START_Y) else: E = E + self.MM_PIXEL * (START_Y - Y) gcode_col += "G1 X%.4f Y%.4f E%.4f F%.4f\n" % ( XMM, YMM, E * E_FACTOR, engrave_speed) if not first_xmm: first_xmm = XMM + 0.1 # little offset needed! first_ymm = YMM * 1 else: if not first_xmm: first_xmm = XMM + 0.1 # little offset needed! first_ymm = YMM * 1 # bit value has changed! if bit == 0: # jump to start of line to write START_Y = Y gcode_col += "G0 X%.4f Y%.4f F%.4f\n" % ( XMM, YMM, travel_speed) last_xmm = None last_ymm = None else: # end of line to write if DIR > 0: E = E + (self.MM_PIXEL * (Y - START_Y)) else: E = E + (self.MM_PIXEL * (START_Y - Y)) gcode_col += "G1 X%.4f Y%.4f E%.4f F%.4f\n" % ( XMM, YMM, E * E_FACTOR, engrave_speed) last_bit = bit if gcode_col <> "": # we skip empty columns # place last position as G0 to be sure to switch off laser immediately at finish of the line! if first_xmm: fo.write("G0 X%.4f Y%.4f F%.4f ; force laser off\n" % ( first_xmm, first_ymm, travel_speed)) fo.write("M400 ; X=%d printing row: direction %i\nG92 E0\n%s" % ( X, DIR, gcode_col)) if self.settings.lc_change_dir: DIR = DIR * (-1) # change y direction on every X if self.settings.lc_melzi_hack: fo.write(GCODE_FOOT_MELZI) else: fo.write(GCODE_FOOT) fo.close() if self.pronterwindow: self.log("") 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 self.log("Converting HPGL plot for lasercut:") self.log("File: %s" % filename) fi = open(filename, "r") fo = open(filename + ".g", "w") if self.settings.lc_melzi_hack: fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD_MELZI)) else: fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD)) G = "0" LASER_STATE = 0 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() if action != "": cmd = action[:2] if cmd == "PD": LASER_STATE = 1 elif cmd == "PU": LASER_STATE = 0 if last_cmd == "PD": OFFSET_X = coord[0] * -1 OFFSET_Y = coord[1] * -1 fo.write("; PD PU detected, set coord offset %.4f x %.4f mm\n" % ( OFFSET_X, OFFSET_Y)) elif cmd == "PA" or cmd == "PR": # TODO: convert relative coordinates to absolute here! coord = action[2:].split(",") coord[0] = (float(coord[0]) + OFFSET_X) * SCALE_FACTOR coord[1] = (float(coord[1]) + OFFSET_Y) * SCALE_FACTOR if LASER_STATE: EN = " E%.4f F%.4f" % ( E_FACTOR * math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]), engrave_speed) else: EN = " F%.4f" % travel_speed fo.write("G%d X%.4f Y%.4f%s\n" % ( LASER_STATE, coord[0], coord[1], EN) ) last_coord = coord elif cmd == "IN": pass elif cmd == "PT": print "Ignoring pen thickness" else: print "UNKNOWN: %s" % action last_cmd = cmd if self.settings.lc_melzi_hack: fo.write(GCODE_FOOT_MELZI) else: fo.write(GCODE_FOOT) fi.close() fo.close() if self.pronterwindow: self.log("") self.pronterwindow.load_gcode_async(filename + '.g') def svg2gcode(self, filename): # Imports for SVG import xml.etree.ElementTree as ET from svg2gcode import shapes as shapes_pkg from svg2gcode.shapes import point_generator from svg2gcode import simplepath, cspsubdiv, cubicsuperpath, simpletransform bed_max_x = float(self.settings.lc_svg_width) bed_max_y = float(self.settings.lc_svg_height) self.log("Generating paths from SVG (outlines only)...") svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) tree = ET.parse(filename) root = tree.getroot() # Todo: force viewbox values configurable? #width = root.get('width') #height = root.get('height') # if width == None or height == None: viewbox = root.get('viewBox') if viewbox: _, _, width, height = viewbox.split() else: # no viewbox element, try to get from svg root element try: width = root.attrib["width"] height = root.attrib["height"] self.log("No ViewBox, got dimensions from root element)") except: width = None height = None if width == None or height == None: self.log("Unable to get width and height for the svg!") return False else: self.log("SVG Dimensions are %s x %s" % (width, height)) # TODO: use cm or mm as absolute dimensions! width = float(width.replace("px", "").replace("pt", "").replace("mm", "")) height = float(height.replace("px", "").replace("pt", "").replace("mm", "")) smoothness = self.settings.lc_svg_smoothness if smoothness < 0.1: smoothness = 0.1 # get the minimum x and y values to get an offset to 0,0 ofs_x = 99999999999.0 ofs_y = 99999999999.0 max_x = -99999999999.0 max_y = -99999999999.0 for elem in root.iter(): try: _, tag_suffix = elem.tag.split('}') except ValueError: continue if tag_suffix in svg_shapes: shape_class = getattr(shapes_pkg, tag_suffix) shape_obj = shape_class(elem) d = shape_obj.d_path() m = shape_obj.transformation_matrix() if d: p = point_generator(d, m, smoothness) start = True for x,y,pen in p: if x < ofs_x: ofs_x = x if y < ofs_y: ofs_y = y if x > max_x: max_x = x if y > max_y: max_y = y if self.settings.lc_svg_offset: ofs_x *= -1 ofs_y *= -1 max_x += ofs_x max_y += ofs_y self.log("Calculated Offset to 0,0 is %f,%f" % (ofs_x, ofs_y)) else: ofs_x = 0 ofs_y = 0 """ self.log("Calculated Dimension is %f,%f" % (max_x, max_y)) width = max_x height = max_y """ if self.settings.lc_svg_scalemode == "original": scale_x = 1.0 scale_y = 1.0 elif self.settings.lc_svg_scalemode == "scale": scale_x = bed_max_x / width scale_y = bed_max_y / height if (scale_x * height) > bed_max_y: # use y scale scale_x = scale_y elif (scale_y * width) > bed_max_x: # use x scale scale_y = scale_x # double-check if (scale_x * width > bed_max_x) or (scale_y * height > bed_max_y): scale_x = scale_y = min(bed_max_x, bed_max_y) / max(width, height) else: scale_x = bed_max_x / width scale_y = bed_max_y / height self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y)) fo = open(filename + ".g", "w") if self.settings.lc_melzi_hack: fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD_MELZI)) fo.write("M571 S0 E1 ; On SVG we control the laser by ourself\n") else: 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 errors = 0 elemidx = 0 for elem in root.iter(): elemidx += 1 try: _, tag_suffix = elem.tag.split('}') except ValueError: continue if tag_suffix in svg_shapes: try: styles = elem.attrib['style'].split(';') except KeyError: styles = [] skip = False for style in styles: style = style.split(':') if style[0] == 'stroke': # ignore all stroke colors which are not #000000 if style[1] != "#000000": self.log("Ignoring shape %s (%i) by stroke color" % (tag_suffix, elemidx)) skip = True break # TODO: do a config option to enter the CUT color or something if skip: continue self.log("Parsing shape: %s (%i)" % (tag_suffix, elemidx)) shape_class = getattr(shapes_pkg, tag_suffix) shape_obj = shape_class(elem) d = shape_obj.d_path() mat = shape_obj.transformation_matrix() if d: if self.settings.lc_melzi_hack: fo.write("M400 ; start %s\n" % (tag_suffix)) fo.write("G92 E0\n") E = 0 xo = 0 yo = 0 idxo = None #p = point_generator(d, mat, smoothness) simple_path = simplepath.parsePath(d) if len(simple_path) == 0: self.log("Path length zero!") continue p = cubicsuperpath.parsePath(d) if mat: simpletransform.applyTransformToPath(mat, p) for sp in p: cspsubdiv.subdiv( sp, smoothness) #self.log("Laser ON at: " + repr(sp[0][0])) x = sp[0][0][0] + ofs_x y = sp[0][0][1] + ofs_y y = height - y # invert the bed xs = scale_x * x ys = scale_y * y if self.settings.lc_melzi_hack: fo.write("M400 ; Wait for all moves to finish\n") fo.write("M42 P28 S0 ; Turn off laser\n") fo.write("G0 X%0.4f Y%0.4f F%.4f ; Move to start of shape\n" % ( xs, ys, travel_speed)) fo.write("M400 ; Wait for all moves to finish\n") fo.write("M42 P28 S255 ; Turn on laser\n") else: fo.write("M5 ; Turn off laser\n") fo.write("G0 X%0.4f Y%0.4f F%.4f ; Move to start of shape\n" % ( xs, ys, travel_speed)) # todo: laser power as parameter? fo.write("M3 S100 ; Turn on laser\n") xo = xs yo = ys object_xs = xs object_ys = ys for csp in sp: ctrl_pt1 = csp[0] ctrl_pt2 = csp[1] end_pt = csp[2] x = end_pt[0] + ofs_x y = end_pt[1] + ofs_y y = height - y # invert the bed xs = round(scale_x * x, 4) ys = round(scale_y * y, 4) if xo == xs and yo == ys: continue #self.log(" Point " + repr(end_pt)) e_distance = math.hypot(xs - xo, ys - yo) xo = xs yo = ys E = E + (e_distance) if xs >= 0 and xs <= bed_max_x+0.1 and ys >= 0 and ys <= bed_max_y+0.1: if self.settings.lc_melzi_hack: fo.write("G1 X%0.4f Y%0.4f E%.4f F%.4f\n" % ( xs, ys, E * E_FACTOR, engrave_speed)) else: fo.write("G1 X%0.4f Y%0.4f F%.4f\n" % ( xs, ys, engrave_speed)) else: if self.settings.lc_melzi_hack: fo.write("G0 X%0.4f Y%0.4f F%.4f\n" % ( xs, ys, travel_speed)) else: fo.write("M5 ; Turn off laser\n") fo.write("G0 X%0.4f Y%0.4f F%.4f\n" % ( xs, ys, travel_speed)) # todo: laser power as parameter? fo.write("M3 S100 ; Turn on laser\n") errors += 1 if errors < 10: self.log("Position outside print dimension: %d, %d" % (xs, ys)) #print " Point: ", end_pt[0], end_pt[1], pen #self.log("Laser OFF at: " + repr(sp[-1][-1])) #if shape_obj.xml_node.get('fill'): if tag_suffix == "polygon": # Close the polygon if self.settings.lc_melzi_hack: e_distance = math.hypot(object_xs - xo, object_ys - yo) E = E + (e_distance) fo.write("G1 X%0.4f Y%0.4f E%.4f F%.4f ; Close the object polygon\n" % ( object_xs, object_ys, E * E_FACTOR, engrave_speed)) else: fo.write("G1 X%0.4f Y%0.4f F%.4f ; Close the object polygon\n" % ( object_xs, object_ys, engrave_speed)) print "connecting filled polygon path end to start" if self.settings.lc_melzi_hack: fo.write(GCODE_FOOT_MELZI) else: fo.write(GCODE_FOOT) fo.close() if errors > 0: self.log("%i errors while generating gcode" % errors) if self.pronterwindow: self.log("") self.pronterwindow.load_gcode_async(filename + '.g')