svg2gcode/svg2gcode.py

changeset 8
86f90bddac0f
parent 7
421aae29d9d9
child 9
89d724cfd8c3
equal deleted inserted replaced
7:421aae29d9d9 8:86f90bddac0f
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 import svg, sys 4 import svg, sys
5 import cairo
6 from gcode import Gcode 5 from gcode import Gcode
7 from optparse import OptionParser 6 from optparse import OptionParser
8 from tinycss import CSS21Parser 7 from tinycss import CSS21Parser
9 8
10 from shapely.geometry import box, MultiLineString, MultiPolygon, Polygon 9 from shapely.geometry import box, MultiLineString, Polygon
11 from shapely.affinity import rotate 10 from shapely.affinity import rotate
12 from shapely import speedups 11 from shapely import speedups
13 from math import sqrt 12 from math import sqrt
14 13
15 # enable Shapely speedups, if possible 14 # enable Shapely speedups, if possible
45 # centred on centroid of the bounding box, 'spacing' units apart 44 # centred on centroid of the bounding box, 'spacing' units apart
46 coords = [] 45 coords = []
47 for i in range(number_of_lines): 46 for i in range(number_of_lines):
48 # alternate lines l2r and r2l to keep HP-7470A plotter happy ☺ 47 # alternate lines l2r and r2l to keep HP-7470A plotter happy ☺
49 if i % 2: 48 if i % 2:
50 coords.extend([((centre_x - hatch_length / 2, centre_y 49 coords.extend([((
51 - hatch_length / 2 + i * spacing), (centre_x 50 centre_x - hatch_length / 2, \
52 + hatch_length / 2, centre_y - hatch_length 51 centre_y - hatch_length / 2 + i * spacing), (
53 / 2 + i * spacing))]) 52 centre_x + hatch_length / 2, \
53 centre_y - hatch_length / 2 + i * spacing))])
54 else: 54 else:
55 coords.extend([((centre_x + hatch_length / 2, centre_y 55 coords.extend([(( \
56 - hatch_length / 2 + i * spacing), (centre_x 56 centre_x + hatch_length / 2, \
57 - hatch_length / 2, centre_y - hatch_length 57 centre_y - hatch_length / 2 + i * spacing), (
58 / 2 + i * spacing))]) 58 centre_x - hatch_length / 2, \
59 centre_y - hatch_length / 2 + i * spacing))])
59 # turn array into Shapely object 60 # turn array into Shapely object
60 lines = MultiLineString(coords) 61 lines = MultiLineString(coords)
61 # Rotate by angle around box centre 62 # Rotate by angle around box centre
62 lines = rotate(lines, angle, origin='centroid', use_radians=False) 63 lines = rotate(lines, angle, origin='centroid', use_radians=False)
63 # return clipped array 64 # return clipped array
64 return rect.intersection(lines) 65 return rect.intersection(lines)
65
66
67 parser = OptionParser()
68 parser.add_option("-f", "--file", dest="filename", default=None,
69 help="Load SVG file", metavar="FILE")
70 parser.add_option("-s", "--scale",
71 dest="scale", type="float", default=1.0,
72 help="set scale factor (default 1.0)")
73 parser.add_option("-e", "",
74 dest="engrave_speed", type="float", default=20,
75 help="engrave speed mm/sec (default 20)")
76 parser.add_option("-t", "",
77 dest="travel_speed", type="float", default=130,
78 help="travel speed mm/sec (default 130)")
79 parser.add_option("-o", "--outline", action="store_true",
80 dest="outline", default=False,
81 help="no infill, only outlines")
82
83
84 (options, args) = parser.parse_args()
85
86
87 if not options.filename:
88 print "no filename given!"
89 sys.exit(1)
90
91 gcode = Gcode(scale=options.scale, travel_speed=options.travel_speed, engrave_speed=options.engrave_speed)
92
93 im = svg.parse(options.filename)
94 b1, b2 = im.bbox()
95 width, height = b2.coord()
96 print "Original dimension: %.2f x %.2f" % (width, height)
97 width *= gcode.mm_pixel * options.scale
98 height *= gcode.mm_pixel * options.scale
99 print "Print dimension: %.2fmm x %.2fmm" % (width, height)
100
101 x1, y1 = b1.coord()
102 x2, y2 = b2.coord()
103 page = box(x1, y1, x2, y2)
104 INFILL = hatchbox(page, 0, 2)
105 66
106 def parse_style(stylestr): 67 def parse_style(stylestr):
107 if stylestr.strip() == '': 68 if stylestr.strip() == '':
108 return None 69 return None
109 parser = CSS21Parser() 70 parser = CSS21Parser()
111 kv = {} 72 kv = {}
112 for obj in style[0]: 73 for obj in style[0]:
113 kv[obj.name] = obj.value[0].value 74 kv[obj.name] = obj.value[0].value
114 return kv 75 return kv
115 76
116 def normalize(coord):
117 x = coord[0]
118 y = coord[1]
119 # flip y
120 y = (b2.coord()[1] - y)
121 return (x, y)
122 77
123 data = im.flatten() 78 def svg2gcode(options, gcode):
124 for d in data:
125 if hasattr(d, "segments"):
126 for l in d.segments(1):
127 # THE OUTLINE
128 x, y = normalize(l[0].coord())
129 gcode.move(x, y)
130 for pt in l[1:]:
131 x, y = normalize(pt.coord())
132 gcode.engrave(x, y)
133 79
134 if options.outline: 80 def normalize(coord):
135 continue 81 x = coord[0]
82 y = coord[1]
83 # flip y
84 y = (b2.coord()[1] - y)
85 return (x, y)
136 86
137 if (isinstance(d, svg.Polygon) or isinstance(d, svg.Path)): 87 im = svg.parse(options.filename)
138 #check if we shoild infill? 88 b1, b2 = im.bbox()
139 style = parse_style(d.style) 89 width, height = b2.coord()
140 if not style: 90 msg = "Original dimension: %.2f x %.2f" % (width, height)
141 continue 91 print msg
142 if not 'fill' in style.keys(): 92 gcode.comment(msg)
143 continue 93 width *= gcode.mm_pixel * options.scale
144 if style['fill'] == 'none': 94 height *= gcode.mm_pixel * options.scale
145 continue 95 msg = "Print dimension: %.2fmm x %.2fmm" % (width, height)
96 print msg
97 gcode.comment(msg)
146 98
147 poly = None 99 x1, y1 = b1.coord()
100 x2, y2 = b2.coord()
101 page = box(x1, y1, x2, y2)
102 # TODO: Infill spacing needs to be calculated with proper scaling and gcode MM dimensions
103 # 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!)
104 INFILL = hatchbox(page, 0, 2)
105
106 data = im.flatten()
107 for d in data:
108 if hasattr(d, "segments"):
148 for l in d.segments(1): 109 for l in d.segments(1):
149 # create shape from polygon: 110 # THE OUTLINE
150 segments = [] 111 x, y = normalize(l[0].coord())
151 for pnt in l: 112 gcode.move(x, y)
152 x, y = pnt.coord() 113 for pt in l[1:]:
153 segments.append((x, y)) 114 x, y = normalize(pt.coord())
154 shape = Polygon(segments) 115 gcode.engrave(x, y)
155 if shape.is_valid: 116
156 if not poly: 117 if options.outline:
157 poly = shape 118 continue
158 else: 119
159 if shape.within(poly): 120 if isinstance(d, svg.Polygon) or isinstance(d, svg.Path):
160 poly = poly.difference(shape) 121 #check if we should infill?
122 style = parse_style(d.style)
123 if not style:
124 continue
125 if not 'fill' in style.keys():
126 continue
127 if style['fill'] == 'none':
128 continue
129
130 # try to generate the infill poly complex
131 poly = None
132 for l in d.segments(1):
133 segments = []
134 for pnt in l:
135 x, y = pnt.coord()
136 segments.append((x, y))
137 shape = Polygon(segments)
138 if shape.is_valid:
139 if not poly:
140 poly = shape
161 else: 141 else:
162 poly = poly.union(shape) 142 if shape.within(poly):
143 poly = poly.difference(shape)
144 else:
145 poly = poly.union(shape)
163 146
164 lines = poly.intersection(INFILL) 147 lines = poly.intersection(INFILL)
165 if lines: 148 if lines:
166 # THE INFILL 149 # THE INFILL
167 for line in lines: 150 for line in lines:
168 start = normalize((line.coords[0][0], line.coords[0][1])) 151 # TODO: swap start/end to nearest move!
169 end = normalize((line.coords[1][0], line.coords[1][1])) 152 start = normalize((line.coords[0][0], line.coords[0][1]))
170 gcode.move(start[0], start[1]) 153 end = normalize((line.coords[1][0], line.coords[1][1]))
171 gcode.engrave(end[0], end[1]) 154 gcode.move(start[0], start[1])
155 gcode.engrave(end[0], end[1])
172 156
173 # write gcode file 157 if __name__ == "__main__":
174 gcode.write(options.filename + ".g") 158 parser = OptionParser()
159 parser.add_option("-f", "--file", dest="filename", default=None,
160 help="Load SVG file", metavar="FILE")
161 parser.add_option("-s", "--scale",
162 dest="scale", type="float", default=1.0,
163 help="set scale factor (default 1.0)")
164 parser.add_option("-e", "",
165 dest="engrave_speed", type="float", default=20,
166 help="engrave speed mm/sec (default 20)")
167 parser.add_option("-t", "",
168 dest="travel_speed", type="float", default=130,
169 help="travel speed mm/sec (default 130)")
170 parser.add_option("-o", "--outline", action="store_true",
171 dest="outline", default=False,
172 help="no infill, only outlines")
173
174 (options, args) = parser.parse_args()
175
176 if not options.filename:
177 print "no filename given!"
178 sys.exit(1)
179
180 # initialize gcode worker
181 gcode = Gcode(scale=options.scale, travel_speed=options.travel_speed, engrave_speed=options.engrave_speed)
182
183 # processing
184 svg2gcode(options, gcode)
185
186 # write gcode file
187 gcode.write(options.filename + ".g")

mercurial