Sat, 07 Nov 2015 20:31:30 +0100
code refactoring - more cleanup
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 | |
6
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
7 | from tinycss import CSS21Parser |
4 | 8 | |
8 | 9 | from shapely.geometry import box, MultiLineString, Polygon |
5 | 10 | from shapely.affinity import rotate |
11 | from shapely import speedups | |
12 | from math import sqrt | |
13 | ||
14 | # enable Shapely speedups, if possible | |
15 | if speedups.available: | |
16 | speedups.enable() | |
17 | ||
18 | def hatchbox(rect, angle, spacing): | |
19 | """ | |
20 | returns a Shapely geometry (MULTILINESTRING, or more rarely, | |
21 | GEOMETRYCOLLECTION) for a simple hatched rectangle. | |
22 | ||
23 | args: | |
24 | rect - a Shapely geometry for the outer boundary of the hatch | |
25 | Likely most useful if it really is a rectangle | |
26 | ||
27 | angle - angle of hatch lines, conventional anticlockwise -ve | |
28 | ||
29 | spacing - spacing between hatch lines | |
30 | ||
31 | GEOMETRYCOLLECTION case occurs when a hatch line intersects with | |
32 | the corner of the clipping rectangle, which produces a point | |
33 | along with the usual lines. | |
34 | """ | |
35 | ||
36 | (llx, lly, urx, ury) = rect.bounds | |
37 | centre_x = (urx + llx) / 2 | |
38 | centre_y = (ury + lly) / 2 | |
39 | diagonal_length = sqrt((urx - llx) ** 2 + (ury - lly) ** 2) | |
40 | number_of_lines = 2 + int(diagonal_length / spacing) | |
41 | hatch_length = spacing * (number_of_lines - 1) | |
42 | ||
43 | # build a square (of side hatch_length) horizontal lines | |
44 | # centred on centroid of the bounding box, 'spacing' units apart | |
45 | coords = [] | |
46 | for i in range(number_of_lines): | |
47 | # alternate lines l2r and r2l to keep HP-7470A plotter happy ☺ | |
48 | if i % 2: | |
8 | 49 | coords.extend([(( |
50 | centre_x - hatch_length / 2, \ | |
51 | centre_y - hatch_length / 2 + i * spacing), ( | |
52 | centre_x + hatch_length / 2, \ | |
53 | centre_y - hatch_length / 2 + i * spacing))]) | |
5 | 54 | else: |
8 | 55 | coords.extend([(( \ |
56 | centre_x + hatch_length / 2, \ | |
57 | centre_y - hatch_length / 2 + i * spacing), ( | |
58 | centre_x - hatch_length / 2, \ | |
59 | centre_y - hatch_length / 2 + i * spacing))]) | |
5 | 60 | # turn array into Shapely object |
61 | lines = MultiLineString(coords) | |
62 | # Rotate by angle around box centre | |
63 | lines = rotate(lines, angle, origin='centroid', use_radians=False) | |
64 | # return clipped array | |
65 | return rect.intersection(lines) | |
66 | ||
6
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
67 | def parse_style(stylestr): |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
68 | if stylestr.strip() == '': |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
69 | return None |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
70 | parser = CSS21Parser() |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
71 | style = parser.parse_style_attr(stylestr) |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
72 | kv = {} |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
73 | for obj in style[0]: |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
74 | kv[obj.name] = obj.value[0].value |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
75 | return kv |
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
76 | |
9 | 77 | class Image(object): |
78 | def __init__(self, filename, options, gcoder): | |
79 | self.gcoder = gcoder | |
80 | self.options = options | |
81 | self.svg = svg.parse(filename) | |
82 | self.bb1, self.bb2 = self.svg.bbox() | |
83 | self.width, self.height = self.bb2.coord() | |
84 | self.infill = None | |
8 | 85 | |
9 | 86 | self._check_dimensions() |
87 | self._generate_infill() | |
88 | ||
89 | def _check_dimensions(self): | |
90 | msg = "Original dimension: %.2f x %.2f" % (self.width, self.height) | |
91 | print msg | |
92 | self.gcoder.comment(msg) | |
93 | width = self.width * self.gcoder.mm_pixel * self.options.scale | |
94 | height = self.height * self.gcoder.mm_pixel * self.options.scale | |
95 | msg = "Print dimension: %.2fmm x %.2fmm" % (width, height) | |
96 | print msg | |
97 | self.gcoder.comment(msg) | |
8 | 98 | |
9 | 99 | def _generate_infill(self): |
100 | b1x, b1y = self.bb1.coord() | |
101 | b2x, b2y = self.bb2.coord() | |
102 | page = box(b1x, b1y, b2x, b2y) | |
103 | # TODO: Infill spacing needs to be calculated with proper scaling and gcode MM dimensions | |
104 | # 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!) | |
105 | self.infill = hatchbox(page, 0, 2) | |
4 | 106 | |
9 | 107 | def normalize(self, coord): |
108 | c_x = coord[0] | |
109 | c_y = coord[1] | |
110 | # flip y | |
111 | c_y = (self.height - c_y) | |
112 | return (c_x, c_y) | |
4 | 113 | |
9 | 114 | def get_drawings(self): |
115 | """ | |
116 | Returns a list of all svg drawings with segments attribute | |
117 | """ | |
118 | data = [] | |
119 | for dwg in self.svg.flatten(): | |
120 | if hasattr(dwg, "segments"): | |
121 | data.append(dwg) | |
122 | return data | |
123 | ||
124 | def svg2gcode(options, gcoder): | |
125 | image = Image(options.filename, options, gcoder) | |
8 | 126 | |
9 | 127 | for dwg in image.get_drawings(): |
128 | for l in dwg.segments(1): | |
129 | # THE OUTLINE | |
130 | coord = image.normalize(l[0].coord()) | |
131 | gcoder.move(coord[0], coord[1]) | |
132 | for pt in l[1:]: | |
133 | coord = image.normalize(pt.coord()) | |
134 | gcoder.engrave(coord[0], coord[1]) | |
8 | 135 | |
9 | 136 | if options.outline: |
137 | continue | |
138 | ||
139 | if isinstance(dwg, svg.Polygon) or isinstance(dwg, svg.Path): | |
140 | #check if we should infill? | |
141 | style = parse_style(dwg.style) | |
142 | if not style: | |
143 | continue | |
144 | if not 'fill' in style.keys(): | |
145 | continue | |
146 | if style['fill'] == 'none': | |
147 | continue | |
6
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
148 | |
9 | 149 | # try to generate the infill poly complex |
150 | poly = None | |
151 | for l in dwg.segments(1): | |
152 | segments = [] | |
153 | for pnt in l: | |
154 | segments.append(pnt.coord()) | |
155 | shape = Polygon(segments) | |
156 | if shape.is_valid: | |
157 | if not poly: | |
158 | poly = shape | |
159 | else: | |
160 | if shape.within(poly): | |
161 | poly = poly.difference(shape) | |
8 | 162 | else: |
9 | 163 | poly = poly.union(shape) |
8 | 164 | |
9 | 165 | lines = poly.intersection(image.infill) |
166 | if lines: | |
167 | # THE INFILL | |
168 | for line in lines: | |
169 | # TODO: swap start/end to nearest move! | |
170 | start = image.normalize((line.coords[0][0], line.coords[0][1])) | |
171 | end = image.normalize((line.coords[1][0], line.coords[1][1])) | |
172 | gcoder.move(start[0], start[1]) | |
173 | gcoder.engrave(end[0], end[1]) | |
6
ff679c15cb0e
infill only on Polygon or Path which have fill style attribute
mbayer
parents:
5
diff
changeset
|
174 | |
9 | 175 | def init_options(): |
8 | 176 | parser = OptionParser() |
177 | parser.add_option("-f", "--file", dest="filename", default=None, | |
178 | help="Load SVG file", metavar="FILE") | |
179 | parser.add_option("-s", "--scale", | |
180 | dest="scale", type="float", default=1.0, | |
181 | help="set scale factor (default 1.0)") | |
182 | parser.add_option("-e", "", | |
183 | dest="engrave_speed", type="float", default=20, | |
184 | help="engrave speed mm/sec (default 20)") | |
185 | parser.add_option("-t", "", | |
186 | dest="travel_speed", type="float", default=130, | |
187 | help="travel speed mm/sec (default 130)") | |
188 | parser.add_option("-o", "--outline", action="store_true", | |
189 | dest="outline", default=False, | |
190 | help="no infill, only outlines") | |
9 | 191 | return parser.parse_args() |
7 | 192 | |
9 | 193 | if __name__ == "__main__": |
194 | (OPTIONS, ARGS) = init_options() | |
195 | if not OPTIONS.filename: | |
8 | 196 | print "no filename given!" |
197 | sys.exit(1) | |
5 | 198 | |
8 | 199 | # initialize gcode worker |
9 | 200 | GCODER = Gcode(scale=OPTIONS.scale, travel_speed=OPTIONS.travel_speed, engrave_speed=OPTIONS.engrave_speed) |
8 | 201 | |
202 | # processing | |
9 | 203 | svg2gcode(OPTIONS, GCODER) |
8 | 204 | |
205 | # write gcode file | |
9 | 206 | GCODER.write(OPTIONS.filename + ".g") |