Sat, 07 Nov 2015 15:51:29 +0100
added generic infill support, alpha stage
4 | 1 | #!/usr/bin/env python |
5 | 2 | # -*- coding: utf-8 -*- |
3 | ||
4 | 4 | import svg, sys |
5 | from gcode import Gcode | |
6 | from optparse import OptionParser | |
7 | ||
5 | 8 | from shapely.geometry import box, MultiLineString, Point, Polygon |
9 | from shapely.affinity import rotate | |
10 | from shapely import speedups | |
11 | from math import sqrt | |
12 | ||
13 | # enable Shapely speedups, if possible | |
14 | if speedups.available: | |
15 | speedups.enable() | |
16 | ||
17 | def hatchbox(rect, angle, spacing): | |
18 | """ | |
19 | returns a Shapely geometry (MULTILINESTRING, or more rarely, | |
20 | GEOMETRYCOLLECTION) for a simple hatched rectangle. | |
21 | ||
22 | args: | |
23 | rect - a Shapely geometry for the outer boundary of the hatch | |
24 | Likely most useful if it really is a rectangle | |
25 | ||
26 | angle - angle of hatch lines, conventional anticlockwise -ve | |
27 | ||
28 | spacing - spacing between hatch lines | |
29 | ||
30 | GEOMETRYCOLLECTION case occurs when a hatch line intersects with | |
31 | the corner of the clipping rectangle, which produces a point | |
32 | along with the usual lines. | |
33 | """ | |
34 | ||
35 | (llx, lly, urx, ury) = rect.bounds | |
36 | centre_x = (urx + llx) / 2 | |
37 | centre_y = (ury + lly) / 2 | |
38 | diagonal_length = sqrt((urx - llx) ** 2 + (ury - lly) ** 2) | |
39 | number_of_lines = 2 + int(diagonal_length / spacing) | |
40 | hatch_length = spacing * (number_of_lines - 1) | |
41 | ||
42 | # build a square (of side hatch_length) horizontal lines | |
43 | # centred on centroid of the bounding box, 'spacing' units apart | |
44 | coords = [] | |
45 | for i in range(number_of_lines): | |
46 | # alternate lines l2r and r2l to keep HP-7470A plotter happy ☺ | |
47 | if i % 2: | |
48 | coords.extend([((centre_x - hatch_length / 2, centre_y | |
49 | - hatch_length / 2 + i * spacing), (centre_x | |
50 | + hatch_length / 2, centre_y - hatch_length | |
51 | / 2 + i * spacing))]) | |
52 | else: | |
53 | coords.extend([((centre_x + hatch_length / 2, centre_y | |
54 | - hatch_length / 2 + i * spacing), (centre_x | |
55 | - hatch_length / 2, centre_y - hatch_length | |
56 | / 2 + i * spacing))]) | |
57 | # turn array into Shapely object | |
58 | lines = MultiLineString(coords) | |
59 | # Rotate by angle around box centre | |
60 | lines = rotate(lines, angle, origin='centroid', use_radians=False) | |
61 | # return clipped array | |
62 | return rect.intersection(lines) | |
63 | ||
64 | def infill(bbox, polygon, angle=0, spacing=10): | |
65 | b1, b2 = bbox | |
66 | x1, y1 = b1.coord() | |
67 | x2, y2 = b2.coord() | |
68 | page = box(x1, y1, x2, y2) | |
69 | hatching = hatchbox(page, angle, spacing) | |
70 | # create shape from polygon: | |
71 | segments = [] | |
72 | for pnt in polygon: | |
73 | x, y = pnt.coord() | |
74 | segments.append((x, y)) | |
75 | ||
76 | shape = Polygon(segments) | |
77 | return shape.intersection(hatching) | |
78 | ||
4 | 79 | parser = OptionParser() |
80 | parser.add_option("-f", "--file", dest="filename", default=None, | |
81 | help="Load SVG file", metavar="FILE") | |
82 | parser.add_option("-s", "--scale", | |
83 | dest="scale", type="float", default=1.0, | |
84 | help="set scale factor (default 1.0)") | |
85 | parser.add_option("-e", "", | |
86 | dest="engrave_speed", type="float", default=20, | |
87 | help="engrave speed mm/sec (default 20)") | |
88 | parser.add_option("-t", "", | |
89 | dest="travel_speed", type="float", default=130, | |
90 | help="travel speed mm/sec (default 130)") | |
5 | 91 | parser.add_option("-o", "--outline", action="store_true", |
92 | dest="outline", default=False, | |
93 | help="no infill, only outlines") | |
4 | 94 | |
95 | ||
96 | (options, args) = parser.parse_args() | |
97 | ||
98 | ||
99 | if not options.filename: | |
100 | print "no filename given!" | |
101 | sys.exit(1) | |
102 | ||
103 | gcode = Gcode(scale=options.scale, travel_speed=options.travel_speed, engrave_speed=options.engrave_speed) | |
104 | ||
105 | im = svg.parse(options.filename) | |
106 | b1, b2 = im.bbox() | |
107 | width, height = b2.coord() | |
108 | print "Original dimension: %.2f x %.2f" % (width, height) | |
109 | width *= gcode.mm_pixel * options.scale | |
110 | height *= gcode.mm_pixel * options.scale | |
111 | print "Print dimension: %.2fmm x %.2fmm" % (width, height) | |
112 | ||
113 | def normalize(coord): | |
114 | x = coord[0] | |
115 | y = coord[1] | |
116 | # flip y | |
117 | y = (b2.coord()[1] - y) | |
118 | return (x, y) | |
119 | ||
120 | data = im.flatten() | |
121 | for d in data: | |
122 | if hasattr(d, "segments"): | |
123 | for l in d.segments(1): | |
5 | 124 | # THE OUTLINE |
4 | 125 | x, y = normalize(l[0].coord()) |
126 | gcode.move(x, y) | |
127 | for pt in l[1:]: | |
128 | x, y = normalize(pt.coord()) | |
129 | gcode.engrave(x, y) | |
130 | ||
5 | 131 | if not options.outline: |
132 | # THE INFILL | |
133 | for line in infill(im.bbox(), l, spacing=2): | |
134 | start = normalize((line.coords[0][0], line.coords[0][1])) | |
135 | end = normalize((line.coords[1][0], line.coords[1][1])) | |
136 | gcode.move(start[0], start[1]) | |
137 | gcode.engrave(end[0], end[1]) | |
138 | ||
4 | 139 | # write gcode file |
140 | gcode.write(options.filename + ".g") |