svg2gcode/svg2gcode.py

Sat, 07 Nov 2015 15:51:29 +0100

author
mbayer
date
Sat, 07 Nov 2015 15:51:29 +0100
changeset 5
b41cdab37aab
parent 4
234ad2069fdd
child 6
ff679c15cb0e
permissions
-rwxr-xr-x

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")

mercurial