Sat, 07 Nov 2015 15:51:29 +0100
added generic infill support, alpha stage
#!/usr/bin/env python # -*- coding: utf-8 -*- import svg, sys from gcode import Gcode from optparse import OptionParser from shapely.geometry import box, MultiLineString, Point, 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 infill(bbox, polygon, angle=0, spacing=10): b1, b2 = bbox x1, y1 = b1.coord() x2, y2 = b2.coord() page = box(x1, y1, x2, y2) hatching = hatchbox(page, angle, spacing) # create shape from polygon: segments = [] for pnt in polygon: x, y = pnt.coord() segments.append((x, y)) shape = Polygon(segments) return shape.intersection(hatching) 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) def normalize(coord): x = coord[0] y = coord[1] # flip y y = (b2.coord()[1] - y) return (x, y) 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 not options.outline: # THE INFILL for line in infill(im.bbox(), l, spacing=2): 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]) # write gcode file gcode.write(options.filename + ".g")