Sat, 07 Nov 2015 20:31:30 +0100
code refactoring - more cleanup
#!/usr/bin/env python # -*- coding: utf-8 -*- import svg, sys 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): if stylestr.strip() == '': return None parser = CSS21Parser() style = parser.parse_style_attr(stylestr) kv = {} for obj in style[0]: kv[obj.name] = obj.value[0].value return kv class Image(object): 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): msg = "Original dimension: %.2f x %.2f" % (self.width, self.height) print msg self.gcoder.comment(msg) 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): 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): 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 for line in lines: # TODO: swap start/end to nearest move! start = image.normalize((line.coords[0][0], line.coords[0][1])) end = image.normalize((line.coords[1][0], line.coords[1][1])) gcoder.move(start[0], start[1]) gcoder.engrave(end[0], end[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")