Sat, 07 Nov 2015 20:50:55 +0100
finished refactoring
#!/usr/bin/env python # -*- coding: utf-8 -*- import svg, sys, math from gcode import Gcode from optparse import OptionParser from tinycss import CSS21Parser from shapely.geometry import box, MultiLineString, Polygon from shapely.affinity import rotate from shapely import speedups from math import sqrt # enable Shapely speedups, if possible if speedups.available: speedups.enable() def hatchbox(rect, angle, spacing): """ returns a Shapely geometry (MULTILINESTRING, or more rarely, GEOMETRYCOLLECTION) for a simple hatched rectangle. args: rect - a Shapely geometry for the outer boundary of the hatch Likely most useful if it really is a rectangle angle - angle of hatch lines, conventional anticlockwise -ve spacing - spacing between hatch lines GEOMETRYCOLLECTION case occurs when a hatch line intersects with the corner of the clipping rectangle, which produces a point along with the usual lines. """ (llx, lly, urx, ury) = rect.bounds centre_x = (urx + llx) / 2 centre_y = (ury + lly) / 2 diagonal_length = sqrt((urx - llx) ** 2 + (ury - lly) ** 2) number_of_lines = 2 + int(diagonal_length / spacing) hatch_length = spacing * (number_of_lines - 1) # build a square (of side hatch_length) horizontal lines # centred on centroid of the bounding box, 'spacing' units apart coords = [] 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))]) 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))]) # turn array into Shapely object lines = MultiLineString(coords) # Rotate by angle around box centre lines = rotate(lines, angle, origin='centroid', use_radians=False) # return clipped array return rect.intersection(lines) def parse_style(stylestr): """ Parse the given string containing CSS2.1 syntax Returns a dict with the keys/values """ if stylestr.strip() == '': return None parser = CSS21Parser() style = parser.parse_style_attr(stylestr) data = {} for obj in style[0]: data[obj.name] = obj.value[0].value return data class Image(object): """ SVG Image handler class """ def __init__(self, filename, options, gcoder): self.gcoder = gcoder self.options = options self.svg = svg.parse(filename) self.bb1, self.bb2 = self.svg.bbox() self.width, self.height = self.bb2.coord() self.infill = None self._check_dimensions() self._generate_infill() def _check_dimensions(self): """ Output image dimensions/scaling to console and gcode """ msg = "Original dimension: %.2f x %.2f" % (self.width, self.height) print msg self.gcoder.comment(msg) self.gcoder.comment("Scale: %.2f" % (self.options.scale)) width = self.width * self.gcoder.mm_pixel * self.options.scale height = self.height * self.gcoder.mm_pixel * self.options.scale msg = "Print dimension: %.2fmm x %.2fmm" % (width, height) print msg self.gcoder.comment(msg) def _generate_infill(self): """ Generates infill pattern image for later use """ b1x, b1y = self.bb1.coord() b2x, b2y = self.bb2.coord() page = box(b1x, b1y, b2x, b2y) # 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!) self.infill = hatchbox(page, 0, 2) def normalize(self, coord): """ Normalize X / Y Axis of coordinates At the moment only Y gets flipped to match Reprap coordinate system (0,0 is bottom left instead top left on SVG) """ c_x = coord[0] c_y = coord[1] # flip y c_y = (self.height - c_y) return (c_x, c_y) def get_drawings(self): """ Returns a list of all svg drawings with segments attribute """ data = [] for dwg in self.svg.flatten(): if hasattr(dwg, "segments"): data.append(dwg) return data def svg2gcode(options, gcoder): image = Image(options.filename, options, gcoder) for dwg in image.get_drawings(): for l in dwg.segments(1): # THE OUTLINE coord = image.normalize(l[0].coord()) gcoder.move(coord[0], coord[1]) for pt in l[1:]: coord = image.normalize(pt.coord()) gcoder.engrave(coord[0], coord[1]) if options.outline: continue if isinstance(dwg, svg.Polygon) or isinstance(dwg, svg.Path): #check if we should infill? style = parse_style(dwg.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 dwg.segments(1): segments = [] for pnt in l: segments.append(pnt.coord()) 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(image.infill) if lines: # THE INFILL prev_end = None for line in lines: coords = [ image.normalize((line.coords[0][0], line.coords[0][1])), image.normalize((line.coords[1][0], line.coords[1][1])) ] if prev_end: # calculate distances to previous end, swap if current end is nearest dist = [ abs(math.hypot(coords[0][0] - prev_end[0], coords[0][1] - prev_end[1])), abs(math.hypot(coords[1][0] - prev_end[0], coords[1][1] - prev_end[1])) ] if dist[0] > dist[1]: coords = list(reversed(coords)) prev_end = coords[1] gcoder.move(coords[0][0], coords[0][1]) gcoder.engrave(coords[1][0], coords[1][1]) def init_options(): 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") return parser.parse_args() if __name__ == "__main__": (OPTIONS, ARGS) = init_options() if not OPTIONS.filename: print "no filename given!" sys.exit(1) # initialize gcode worker GCODER = Gcode(scale=OPTIONS.scale, travel_speed=OPTIONS.travel_speed, engrave_speed=OPTIONS.engrave_speed) # processing svg2gcode(OPTIONS, GCODER) # write gcode file GCODER.write(OPTIONS.filename + ".g")