svg2gcode/svg2gcode.py

Sat, 07 Nov 2015 20:31:30 +0100

author
mbayer
date
Sat, 07 Nov 2015 20:31:30 +0100
changeset 9
89d724cfd8c3
parent 8
86f90bddac0f
child 10
92835c3f171a
permissions
-rwxr-xr-x

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

mercurial