Sat, 07 Nov 2015 19:30:27 +0100
code refactoring
svg2gcode/gcode.py | file | annotate | diff | comparison | revisions | |
svg2gcode/svg2gcode.py | file | annotate | diff | comparison | revisions |
--- a/svg2gcode/gcode.py Sat Nov 07 18:45:17 2015 +0100 +++ b/svg2gcode/gcode.py Sat Nov 07 19:30:27 2015 +0100 @@ -1,17 +1,13 @@ import math INCH = 25.4 # mm +STEPS_PER_MM = 80 # hardware: steps/mm on XY class Gcode(object): - def __init__(self, bbox=None, scale=1.0, travel_speed=20, engrave_speed=20): + def __init__(self, scale=1.0, travel_speed=20, engrave_speed=20): self.dpi = 300 self.e_factor = 0.1 - self.mm_pixel = round(INCH / self.dpi, 4) - self.steps_pixel = self.mm_pixel * 80 # mine is 80 steps/mm on XY - print "Resolution is %f mm per pixel" % self.mm_pixel - if self.steps_pixel <= 5: - print "Warning: Steps per pixel (needs to be > 5, otherwise marlin joins lines): %f" % self.steps_pixel self.lines = [] self.dist_start = (0, 0) @@ -21,6 +17,14 @@ self.travel_speed = travel_speed * 60 self.engrave_speed = engrave_speed * 60 + self._check_resolution() + + def _check_resolution(self): + steps_pixel = self.mm_pixel * STEPS_PER_MM + print "Resolution is %f mm per pixel" % self.mm_pixel + if steps_pixel <= 5: + print "Warning: Steps per pixel (needs to be > 5, otherwise marlin joins lines): %f" % steps_pixel + def _dimensions(self, x, y): x = self.mm_pixel * x y = self.mm_pixel * y @@ -29,6 +33,9 @@ y = y * self.scale return (x, y) + def comment(self, msg): + self.lines.append("; %s" % msg) + def move(self, x, y): x, y = self._dimensions(x, y) if self.e_factor > 0: @@ -56,10 +63,9 @@ # write gcode file fout = open(filename, "w") - fout.write(""" -; Filename: %s -; GCode generated by bitplotter one-night-quick-hack script (marlin code flavour) - + fout.write("""; Filename: %s +; GCode generated by one-night-and-several-days-not-so-quick-hack-script (marlin code flavour) +; https://neo-soft.org/hg-public/lasercut-scripts 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
--- a/svg2gcode/svg2gcode.py Sat Nov 07 18:45:17 2015 +0100 +++ b/svg2gcode/svg2gcode.py Sat Nov 07 19:30:27 2015 +0100 @@ -2,12 +2,11 @@ # -*- coding: utf-8 -*- import svg, sys -import cairo from gcode import Gcode from optparse import OptionParser from tinycss import CSS21Parser -from shapely.geometry import box, MultiLineString, MultiPolygon, Polygon +from shapely.geometry import box, MultiLineString, Polygon from shapely.affinity import rotate from shapely import speedups from math import sqrt @@ -47,15 +46,17 @@ for i in range(number_of_lines): # alternate lines l2r and r2l to keep HP-7470A plotter happy ☺ if i % 2: - coords.extend([((centre_x - hatch_length / 2, centre_y - - hatch_length / 2 + i * spacing), (centre_x - + hatch_length / 2, centre_y - hatch_length - / 2 + i * spacing))]) + coords.extend([(( + centre_x - hatch_length / 2, \ + centre_y - hatch_length / 2 + i * spacing), ( + centre_x + hatch_length / 2, \ + centre_y - hatch_length / 2 + i * spacing))]) else: - coords.extend([((centre_x + hatch_length / 2, centre_y - - hatch_length / 2 + i * spacing), (centre_x - - hatch_length / 2, centre_y - hatch_length - / 2 + i * spacing))]) + coords.extend([(( \ + centre_x + hatch_length / 2, \ + centre_y - hatch_length / 2 + i * spacing), ( + centre_x - hatch_length / 2, \ + centre_y - hatch_length / 2 + i * spacing))]) # turn array into Shapely object lines = MultiLineString(coords) # Rotate by angle around box centre @@ -63,46 +64,6 @@ # return clipped array return rect.intersection(lines) - -parser = OptionParser() -parser.add_option("-f", "--file", dest="filename", default=None, - help="Load SVG file", metavar="FILE") -parser.add_option("-s", "--scale", - dest="scale", type="float", default=1.0, - help="set scale factor (default 1.0)") -parser.add_option("-e", "", - dest="engrave_speed", type="float", default=20, - help="engrave speed mm/sec (default 20)") -parser.add_option("-t", "", - dest="travel_speed", type="float", default=130, - help="travel speed mm/sec (default 130)") -parser.add_option("-o", "--outline", action="store_true", - dest="outline", default=False, - help="no infill, only outlines") - - -(options, args) = parser.parse_args() - - -if not options.filename: - print "no filename given!" - sys.exit(1) - -gcode = Gcode(scale=options.scale, travel_speed=options.travel_speed, engrave_speed=options.engrave_speed) - -im = svg.parse(options.filename) -b1, b2 = im.bbox() -width, height = b2.coord() -print "Original dimension: %.2f x %.2f" % (width, height) -width *= gcode.mm_pixel * options.scale -height *= gcode.mm_pixel * options.scale -print "Print dimension: %.2fmm x %.2fmm" % (width, height) - -x1, y1 = b1.coord() -x2, y2 = b2.coord() -page = box(x1, y1, x2, y2) -INFILL = hatchbox(page, 0, 2) - def parse_style(stylestr): if stylestr.strip() == '': return None @@ -113,62 +74,114 @@ kv[obj.name] = obj.value[0].value return kv -def normalize(coord): - x = coord[0] - y = coord[1] - # flip y - y = (b2.coord()[1] - y) - return (x, y) + +def svg2gcode(options, gcode): + + def normalize(coord): + x = coord[0] + y = coord[1] + # flip y + y = (b2.coord()[1] - y) + return (x, y) + + im = svg.parse(options.filename) + b1, b2 = im.bbox() + width, height = b2.coord() + msg = "Original dimension: %.2f x %.2f" % (width, height) + print msg + gcode.comment(msg) + width *= gcode.mm_pixel * options.scale + height *= gcode.mm_pixel * options.scale + msg = "Print dimension: %.2fmm x %.2fmm" % (width, height) + print msg + gcode.comment(msg) -data = im.flatten() -for d in data: - if hasattr(d, "segments"): - for l in d.segments(1): - # THE OUTLINE - x, y = normalize(l[0].coord()) - gcode.move(x, y) - for pt in l[1:]: - x, y = normalize(pt.coord()) - gcode.engrave(x, y) + x1, y1 = b1.coord() + x2, y2 = b2.coord() + page = box(x1, y1, x2, y2) + # TODO: Infill spacing needs to be calculated with proper scaling and gcode MM dimensions + # TODO: Make infill angle 0, 45 or 90 degrees configurable to options parser (0° = X, 90° = Y, 45° = X and Y but half the speed/accel needed!) + INFILL = hatchbox(page, 0, 2) - if options.outline: - continue + data = im.flatten() + for d in data: + if hasattr(d, "segments"): + for l in d.segments(1): + # THE OUTLINE + x, y = normalize(l[0].coord()) + gcode.move(x, y) + for pt in l[1:]: + x, y = normalize(pt.coord()) + gcode.engrave(x, y) + + if options.outline: + continue + + if isinstance(d, svg.Polygon) or isinstance(d, svg.Path): + #check if we should infill? + style = parse_style(d.style) + if not style: + continue + if not 'fill' in style.keys(): + continue + if style['fill'] == 'none': + continue - if (isinstance(d, svg.Polygon) or isinstance(d, svg.Path)): - #check if we shoild infill? - style = parse_style(d.style) - if not style: - continue - if not 'fill' in style.keys(): - continue - if style['fill'] == 'none': - continue + # try to generate the infill poly complex + poly = None + for l in d.segments(1): + segments = [] + for pnt in l: + x, y = pnt.coord() + segments.append((x, y)) + shape = Polygon(segments) + if shape.is_valid: + if not poly: + poly = shape + else: + if shape.within(poly): + poly = poly.difference(shape) + else: + poly = poly.union(shape) + + lines = poly.intersection(INFILL) + if lines: + # THE INFILL + for line in lines: + # TODO: swap start/end to nearest move! + start = normalize((line.coords[0][0], line.coords[0][1])) + end = normalize((line.coords[1][0], line.coords[1][1])) + gcode.move(start[0], start[1]) + gcode.engrave(end[0], end[1]) - poly = None - for l in d.segments(1): - # create shape from polygon: - segments = [] - for pnt in l: - x, y = pnt.coord() - segments.append((x, y)) - shape = Polygon(segments) - if shape.is_valid: - if not poly: - poly = shape - else: - if shape.within(poly): - poly = poly.difference(shape) - else: - poly = poly.union(shape) +if __name__ == "__main__": + parser = OptionParser() + parser.add_option("-f", "--file", dest="filename", default=None, + help="Load SVG file", metavar="FILE") + parser.add_option("-s", "--scale", + dest="scale", type="float", default=1.0, + help="set scale factor (default 1.0)") + parser.add_option("-e", "", + dest="engrave_speed", type="float", default=20, + help="engrave speed mm/sec (default 20)") + parser.add_option("-t", "", + dest="travel_speed", type="float", default=130, + help="travel speed mm/sec (default 130)") + parser.add_option("-o", "--outline", action="store_true", + dest="outline", default=False, + help="no infill, only outlines") - lines = poly.intersection(INFILL) - if lines: - # THE INFILL - for line in lines: - start = normalize((line.coords[0][0], line.coords[0][1])) - end = normalize((line.coords[1][0], line.coords[1][1])) - gcode.move(start[0], start[1]) - gcode.engrave(end[0], end[1]) + (options, args) = parser.parse_args() + + if not options.filename: + print "no filename given!" + sys.exit(1) -# write gcode file -gcode.write(options.filename + ".g") + # initialize gcode worker + gcode = Gcode(scale=options.scale, travel_speed=options.travel_speed, engrave_speed=options.engrave_speed) + + # processing + svg2gcode(options, gcode) + + # write gcode file + gcode.write(options.filename + ".g")