1 #!/usr/bin/env python2.7
  2 #KeyboardCAD by Nicholas Stamplecoskie 2015-01-11
  3 #
  4 #KeyboardCAD is a tool for making FreeCAD files for custom keyboards.
  5 #It uses the raw data generated by http://www.keyboard-layout-editor.com/ to design a switch mounting plate for any keyboard imaginable.
  6 #
  7 #Thanks to Ian Prest for keyboard-layout-editor and to
  8 #Juergen Riegel, Werner Mayer, Yorik van Havre and everyone else involved with FreeCAD
  9 
 10 ###########################################################################
 11 #USER PARAMETERS
 12 ###########################################################################
 13 fileName = 'custom_01'  #name of the FreeCAD file which will be the result of this script. An extension will be added if not present
 14 savePath = '/home/iso/keyboard/' #path indicating where you want to save the file
 15 layoutPath = '/home/iso/keyboard/layout.txt' # path to a text file containing the raw data from http://www.keyboard-layout-editor.com/
 16 plateXDim = 285.75 #overall length of the plate to be made, in mm.
 17 plateYDim = 95.25 #overall width of the plate to be made, in mm.
 18 xStart = 0 #How far from the left edge the keyboard will start to be drawn. Not the distance to the first hole, but to the first key.
 19 yStart = 0 #How far from the top edge the keyboard will start to be drawn.
 20 plateThickness = 1.5 #plate thickness in mm.
 21 includeCutOuts = True #include four cutouts on the plate around each switch for the disassembly of switches while they are installed
 22 rotateSwitches = False #rotate all switches with cutouts by 90 degrees (so that cutouts are on the top and bottom)
 23 includeStabilizers = "both"  #make cutouts for stabilizers? Possible values: False, "costar", "cherry", "both"
 24 
 25 ###########################################################################
 26 #LABEL PARAMETERS
 27 ###########################################################################
 28 #Include these strings in the labels per key in the layout editor
 29 #!r! for rotating a switch with cutouts by 90 degrees (so that cutouts are on the top and bottom)
 30 #!c! for toggling the presence of a cutout on a switch
 31 
 32 ###########################################################################
 33 #MEASUREMENTS
 34 ###########################################################################
 35 #SWITCH
 36 KEYUNIT = 19.05 #the value of one unit
 37 SWITCHSIZE = 13.9954 #in mm, each switch cut-out is a square with sides of this length. 13.9954 is for Cherry MX switches.
 38 #CUTOUTS
 39 CUTOUTLENGTH = 3.81 #each switch can have four cut-outs around it for switch disassembly.
 40 CUTOUTWIDTH = 1.016
 41 CUTOUTSEPARATION = 5.3594 #distance between two cutouts on the same side of a switch
 42 CUTOUTDST = (SWITCHSIZE - (2*CUTOUTLENGTH + CUTOUTSEPARATION))/2 #distance from top side of switch to the top of the first cutout. 0.508
 43 #STABILIZERS
 44 MINIMUMLONGSTABLENGTH = 3 #If key is this wide or wider, use the long stabilizer. Since keys between 3 and units 6 units are unusual, this value can vary without changing anything.
 45 COSTARLENGTH = 13.97 #Length (or height) of the cutout for costar stabilizers
 46 COSTARWIDTH = 3.3 #Width of the cutout for costar stabilizers
 47 COSTARSHORTSEPARATION = 20.57 #Distance between the two stabilizer cutouts that are on either side of the switch. NOT the distance between the stabilizer's stems.
 48 COSTARLONGSEPARATION = 96.774 #Short is for 2 - 3 unit wide keys. Long is for a spacebar.
 49 COSTARLONGSEPARATION2 = False # If you want two possible spacebar stabilizer positions to be cut out, add a value here.
 50 COSTARDST = 0.76 #Distance from the top of the switch and the top of the stabilizer cutout. Make negative to have the stabilizers 'above' the switch.
 51 CHERRYLENGTH = 12.294 #Length (or height) of the cutout for cherry stabilizers
 52 CHERRYWIDTH = 6.655 #Width of the cutout for cherry stabilizers
 53 CHERRYSHORTSEPARATION = 17.22 #Distance between the two stabilizer cutouts that are on either side of the switch. NOT the distance between the stabilizer's stems.
 54 CHERRYLONGSEPARATION = 93.421 #Short is for 2 - 3 unit wide keys. Long is for a spacebar.
 55 CHERRYLONGSEPARATION2 = False # If you want two possible spacebar stabilizer positions to be cut out, add a value here.
 56 CHERRYDST = 1.3 #Distance from the top of the switch and the top of the stabilizer cutout. Make negative to have the stabilizers 'above' the switch.
 57 WIREWIDTH = 2.794 #for cherry only. Width of the cutout that connects the switch cutout to the stab cutout for wire installation.
 58 WIREDST = 4.7 #cherry only. distance from top of switch cutout to top of the wire cutout.
 59 WIREADDLENGTH = 0.89 #cherry only. Distance that the wire cutout extends past the stab cutout.
 60 ADDCUTFORSHORT = True #cherry only. more area beside the switch will be cut for short cherry stabilizers with cutouts present, if this is true. May not be desired for different switch/cutout sizes
 61 
 62 ###########################################################################
 63 #PATHS
 64 ###########################################################################
 65 #Assign values to these two variables if you are not running the script from the python in the FreeCAD folder
 66 FREECADPATH = "/usr/bin/freecad"
 67 FREECADPATH2 = "/usr/lib/freecad/"
 68 
 69 ###########################################################################
 70 #(OPTIONAL) SCREW HOLES
 71 ###########################################################################
 72 #This list of holes must contain tuples (x,y) for the coordinates for where each screw hole will be cut
 73 #x is the distance from the left edge of the plate to the center of the screw hole.
 74 #y is the distance from the TOP edge of the plate to the center of the hole.
 75 # screws = [(20,40),(100,40),(20,100),(100,100)] This is the expected format.
 76 screws = []
 77 screwHoleRadius = 1 #the radius of each hole in mm
 78 
 79 
 80 def main():
 81 	getLayoutData()
 82 	initializeCAD()
 83 	drawSwitches()
 84 	drawStabilizers()
 85 	drawScrewHoles()
 86 	save()
 87 
 88 
 89 #FreeCAD methods
 90 
 91 def	initializeCAD():
 92 	global doc
 93 	doc = FreeCAD.newDocument() #initialize the document
 94 
 95 	pad(sketchRectangle(0, 0, plateXDim, plateYDim, False))	 #draw the plate
 96 
 97 def sketchRectangle(posX, posY, xDim, yDim, rotation):
 98 	global doc
 99 	global sketchCount
100 
101 	rectangle = doc.addObject('Sketcher::SketchObject','Sketch' + str(sketchCount))
102 
103 	if not sketchCount == 0: #if it is the first sketch, then the pad doesn't exist yet
104 		rectangle.Support = (doc.Pad,["Face6"])
105 
106 	sketchCount = sketchCount + 1
107 
108 	#   0_______________3
109 	#	|				|
110 	#	|				|
111 	#	|				|
112 	#	|				|
113 	#	|				|
114 	#	|				|
115 	#  1|_______________|2
116 
117 	x = [0]*4
118 	y = [0]*4
119 
120 	x[0] = posX
121 	y[0] = -posY
122 	x[2] = posX + xDim
123 	y[2] = -posY - yDim
124 
125 	x[1] = x[0]
126 	y[1] = y[2]
127 	x[3] = x[2]
128 	y[3] = y[0]
129 
130 	if rotation:
131 		for n in range(4):
132 			x[n], y[n] = rotatePoint((rotation[0],rotation[1]), (x[n],y[n]), rotation[2])
133 
134 	for i in range(3):
135 		rectangle.addGeometry(Part.Line(App.Vector(x[i],y[i],0),App.Vector(x[i+1],y[i+1],0)))
136 	rectangle.addGeometry(Part.Line(App.Vector(x[3],y[3],0),App.Vector(x[0],y[0],0)))
137 
138 	for j in range(3):
139 		rectangle.addConstraint(Sketcher.Constraint('Coincident',j,2,j+1,1))
140 	rectangle.addConstraint(Sketcher.Constraint('Coincident',3,2,0,1))
141 
142 	for k in range(4):
143 		rectangle.addConstraint(Sketcher.Constraint('DistanceX',k,1,x[k]))
144 		rectangle.addConstraint(Sketcher.Constraint('DistanceY',k,1,y[k]))
145 
146 	doc.recompute()
147 
148 	return rectangle
149 
150 def sketchSwitchWithCutOuts(posX, posY, rotation, rotate90):
151 	global doc
152 	global sketchCount
153 
154 	rectangle = doc.addObject('Sketcher::SketchObject','Sketch' + str(sketchCount))
155 
156 	rectangle.Support = (doc.Pad,["Face6"])
157 
158 	sketchCount = sketchCount + 1
159 
160 	#   0_______________ 19
161 	#2 _|1			  18|_  17
162 	# |					  |
163 	#3|_ 4			  15 _| 16
164 	#6 _|5			  14|_ 13
165 	# |				      |
166 	#7|_ 8			  11 _| 12
167 	# 9 |_______________| 10
168 
169 	x = [0]*20
170 	y = [0]*20
171 
172 	x[0] = posX
173 	y[0] = -posY
174 	x[1] = x[0]
175 	y[1] = y[0] - CUTOUTDST
176 	x[2] = x[0] - CUTOUTWIDTH
177 	y[2] = y[1]
178 	x[3] = x[2]
179 	y[3] = y[2] - CUTOUTLENGTH
180 	x[4] = x[0]
181 	y[4] = y[3]
182 	x[5] = x[0]
183 	y[5] = y[4] - CUTOUTSEPARATION
184 	x[6] = x[2]
185 	y[6] = y[5]
186 	x[7] = x[2]
187 	y[7] = y[6] - CUTOUTLENGTH
188 	x[8] = x[0]
189 	y[8] = y[7]
190 	x[9] = x[0]
191 	y[9] = y[8] - CUTOUTDST # -posY - SWITCHSIZE
192 	x[10] = posX + SWITCHSIZE
193 	y[10] = -posY - SWITCHSIZE
194 	x[11] = x[10]
195 	y[11] = y[8]
196 	x[12] = x[10] + CUTOUTWIDTH
197 	y[12] = y[11]
198 	x[13] = x[12]
199 	y[13] = y[6]
200 	x[14] = x[10]
201 	y[14] = y[13]
202 	x[15] = x[10]
203 	y[15] = y[4]
204 	x[16] = x[12]
205 	y[16] = y[15]
206 	x[17] = x[12]
207 	y[17] = y[2]
208 	x[18] = x[10]
209 	y[18] = y[1]
210 	x[19] = x[10]
211 	y[19] = y[0]
212 
213 	if rotate90:
214 		centerPoint = (x[0] + SWITCHSIZE/2, y[0] - SWITCHSIZE/2)
215 		for n in range(20):
216 			x[n], y[n] = rotatePoint(centerPoint, (x[n],y[n]), 90)
217 
218 	if rotation:
219 		for n in range(20):
220 			x[n], y[n] = rotatePoint((rotation[0],rotation[1]), (x[n],y[n]), rotation[2])
221 
222 	for i in range(19):
223 		rectangle.addGeometry(Part.Line(App.Vector(x[i],y[i],0),App.Vector(x[i+1],y[i+1],0)))
224 	rectangle.addGeometry(Part.Line(App.Vector(x[19],y[19],0),App.Vector(x[0],y[0],0)))
225 
226 	for j in range(19):
227 		rectangle.addConstraint(Sketcher.Constraint('Coincident',j,2,j+1,1))
228 	rectangle.addConstraint(Sketcher.Constraint('Coincident',19,2,0,1))
229 
230 	for k in range(20):
231 		rectangle.addConstraint(Sketcher.Constraint('DistanceX',k,1,x[k]))
232 		rectangle.addConstraint(Sketcher.Constraint('DistanceY',k,1,y[k]))
233 	doc.recompute()
234 	return rectangle
235 
236 def sketchCircle(posX, posY, radius):
237 	global doc
238 	global sketchCount
239 	circle = doc.addObject('Sketcher::SketchObject','Sketch' + str(sketchCount))
240 	circle.Support = (doc.Pad,["Face6"])
241 	sketchCount = sketchCount + 1
242 	posY = -posY
243 	circle.addGeometry(Part.Circle(App.Vector(posX,posY,0),App.Vector(0,0,1),radius))
244 	circle.addConstraint(Sketcher.Constraint('DistanceX',0,3,posX))
245 	circle.addConstraint(Sketcher.Constraint('DistanceY',0,3,posY))
246 	circle.addConstraint(Sketcher.Constraint('Radius',0,radius))
247 	doc.recompute()
248 	return circle
249 
250 def pocket(sketch):
251 	global doc
252 	pocket = doc.addObject("PartDesign::Pocket","Pocket" + str(sketchCount - 1))
253 	pocket.Sketch = sketch
254 	pocket.Length = 5.0
255 	pocket.Type = 1
256 	pocket.UpToFace = None
257 	doc.recompute()
258 
259 def pad(sketch):
260 	global doc
261 	pad = doc.addObject("PartDesign::Pad","Pad")
262 	pad.Sketch = sketch
263 	pad.Length = plateThickness
264 	pad.Reversed = 0
265 	pad.Midplane = 0
266 	pad.Length2 = 100.000000
267 	pad.Type = 0
268 	pad.UpToFace = None
269 	doc.recompute()
270 
271 #Drawing methods
272 def drawSwitches():
273 	for prop in props:
274 		coord = findCoord(prop[0], prop[1], prop[2], prop[3])
275 		rotation = prop[4]
276 		if "!c!" in labels[props.index(prop)]:
277 			drawSwitchWithCutOuts(coord[0], coord[1], rotation, "!r!" in labels[props.index(prop)])
278 		else:
279 			drawSwitch(coord[0], coord[1], rotation)
280 
281 def drawStabilizers():
282 	if includeStabilizers:
283 		if includeStabilizers == "cherry":
284 			drawStabilizersHelper(True)
285 		elif includeStabilizers == "costar":
286 			drawStabilizersHelper(False)
287 		elif includeStabilizers == "both":
288 			drawStabilizersHelper(True)
289 			drawStabilizersHelper(False)
290 		else:
291 			print(includeStabilizers + " is not a valid value for includeStabilizers")
292 			return
293 
294 def drawScrewHoles():
295 	for screw in screws:
296 		pocket(sketchCircle(screw[0], screw[1], screwHoleRadius))
297 
298 def drawSwitch(x, y, rotation):
299 	pocket(sketchRectangle(x, y, SWITCHSIZE, SWITCHSIZE, rotation))
300 
301 def drawSwitchWithCutOuts(x, y, rotation, rotate90):
302 	pocket(sketchSwitchWithCutOuts(x, y, rotation, rotate90))
303 
304 def drawStabilizersHelper(cherry):
305 	for prop in props:
306 		if prop[2] >= 2 or prop[3] >= 2:
307 			coord = findCoord(prop[0], prop[1], prop[2], prop[3])
308 			cutout = "!c!" in labels[props.index(prop)]
309 			rotated90 = "!r!" in labels[props.index(prop)]
310 			rotation = prop[4]
311 			if prop[2] >= MINIMUMLONGSTABLENGTH:#spacebar
312 				drawHorizontalStabilizer(coord[0], coord[1], False, cherry, cutout, rotated90, rotation)
313 				global CHERRYLONGSEPARATION
314 				global COSTARLONGSEPARATION
315 				global CHERRYLONGSEPARATION2
316 				global COSTARLONGSEPARATION2
317 				if cherry and CHERRYLONGSEPARATION2 or not(cherry) and COSTARLONGSEPARATION2: #if a second value is given, swap and then draw again.
318 					tmpCherry = CHERRYLONGSEPARATION
319 					tmpCostar = COSTARLONGSEPARATION
320 					CHERRYLONGSEPARATION = CHERRYLONGSEPARATION2
321 					COSTARLONGSEPARATION = COSTARLONGSEPARATION2
322 					CHERRYLONGSEPARATION2 = tmpCherry
323 					COSTARLONGSEPARATION2 = tmpCostar
324 					drawHorizontalStabilizer(coord[0], coord[1], False, cherry, cutout, rotated90, rotation)
325 			elif prop[3] >= 2: #if taller than 2, stab will be vertical, for iso, big-ass enter, + on numpad, etc..
326 				drawVerticalStabilizer(coord[0], coord[1], cherry, cutout, rotated90, rotation)
327 			else: #standard wide key
328 				drawHorizontalStabilizer(coord[0], coord[1], True, cherry, cutout, rotated90, rotation)
329 
330 def drawHorizontalStabilizer(x, y, short, cherry, cutout, rotated90, rotation):
331 	if cherry:
332 		width = CHERRYWIDTH
333 		length = CHERRYLENGTH
334 		if short:
335 			separation = CHERRYSHORTSEPARATION
336 		else:
337 			separation = CHERRYLONGSEPARATION
338 		dst = CHERRYDST
339 	else:
340 		width = COSTARWIDTH
341 		length = COSTARLENGTH
342 		if short:
343 			separation = COSTARSHORTSEPARATION
344 		else:
345 			separation = COSTARLONGSEPARATION
346 			separation2 = COSTARLONGSEPARATION2
347 		dst = COSTARDST
348 
349 	y = y + dst
350 	xLeft = x + SWITCHSIZE/2 - separation/2 - width
351 	xRight = x + SWITCHSIZE/2 + separation/2
352 
353 	pocket(sketchRectangle(xLeft, y, width, length, rotation))
354 	pocket(sketchRectangle(xRight, y, width, length, rotation))
355 
356 	if cherry:
357 		xWire = xLeft - WIREADDLENGTH
358 		yWire = y + WIREDST
359 		wWire = separation + 2*width + 2*WIREADDLENGTH
360 		pocket(sketchRectangle(xWire, yWire, wWire, WIREWIDTH, rotation))
361 		if cutout and not(rotated90) and short and ADDCUTFORSHORT: #Another cut will be performed since the remaining plate will be very narrow in this area, if cutouts.
362 			pocket(sketchRectangle(xLeft + width, y, separation, length, rotation))
363 
364 def drawVerticalStabilizer(x, y, cherry, cutout, rotated90, rotation):
365 	if cherry:
366 		width = CHERRYWIDTH
367 		length = CHERRYLENGTH
368 		separation = CHERRYSHORTSEPARATION
369 		dst = CHERRYDST
370 	else:
371 		width = COSTARWIDTH
372 		length = COSTARLENGTH
373 		separation = COSTARSHORTSEPARATION
374 		dst = COSTARDST
375 
376 	x = x + dst
377 
378 	yTop = y + SWITCHSIZE/2 - separation/2 - width
379 	pocket(sketchRectangle(x, yTop, length, width, rotation))
380 
381 	yBottom = y + SWITCHSIZE/2 + separation/2
382 	pocket(sketchRectangle(x, yBottom, length, width, rotation))
383 
384 	if cherry:
385 		yWire = yTop - WIREADDLENGTH
386 		xWire = x + WIREDST
387 		hWire = separation + 2*width + 2*WIREADDLENGTH
388 		pocket(sketchRectangle(xWire, yWire, WIREWIDTH, hWire, rotation))
389 		if cutout and rotated90 and ADDCUTFORSHORT: #Another cut will be performed since the remaining plate will be very narrow in this area. only if rotated and cutouts.
390 			pocket(sketchRectangle(x, yTop + width, length, separation, rotation))
391 
392 #Calculation methods
393 def findCoord(x, y, w, h): #calculates where the top left corner of the switch is, given that each switch will be in the exact middle of the key
394 	x = x*KEYUNIT
395 	y = y*KEYUNIT
396 	w = w*KEYUNIT
397 	h = h*KEYUNIT
398 	xPos = x + w/2 - SWITCHSIZE/2
399 	yPos = y + h/2 - SWITCHSIZE/2
400 	xPos = xPos + xStart
401 	yPos = yPos + yStart
402 	return (xPos, yPos)
403 
404 
405 def rotatePoint(centerPoint, point, angle):
406 	tempPoint = (point[0]-centerPoint[0], point[1]-centerPoint[1])
407 	if angle == 10:
408 		tempPoint = (-tempPoint[1], tempPoint[0])
409 	else:
410 		angle = math.radians(angle)
411 		tempPoint = (tempPoint[0]*math.cos(angle)-tempPoint[1]*math.sin(angle), tempPoint[0]*math.sin(angle)+tempPoint[1]*math.cos(angle))
412 	tempPoint = (tempPoint[0]+centerPoint[0], tempPoint[1]+centerPoint[1])
413 	return tempPoint
414 
415 #Input data methods
416 def getLayoutData():
417 	parseLayout(readFile())
418 	fixRotations()
419 	modifyLabels()
420 
421 def readFile():
422 	return open(layoutPath, 'r').readlines()
423 
424 def parseLayout(layoutList):
425 	global labels
426 	for row in layoutList:
427 		newRow = True
428 		row = row.rstrip()
429 		if row[-1:] == ',':
430 			row = row[1:-2]
431 		else:
432 			row = row[1:-1]
433 		values = row.split(",")
434 		tmp = ''
435 		for value in values:
436 			if not tmp == '':
437 				value = tmp + "," + value
438 			if value[:1] == '{' and value[-1:] == '}': #value is a prop
439 				makeProp(value[1:-1], newRow)
440 				newRow = False
441 				tmp = ''
442 			elif value[:1] == '"' and value[-1:] == '"' and not len(value) == 1: #value is a label
443 				labels.append(value[1:-1])
444 				if len(props) < len(labels): #if no prop exists for this label, makes one
445 					makeProp('', newRow)
446 				newRow = False
447 				tmp = ''
448 			else: #splitting of the row into values didn't work right because a value contained a comma
449 				tmp = value
450 
451 def makeProp(values, newRow):
452 	global props
453 	if props:
454 		prevProp = props[-1]
455 	else:
456 		prevProp = (0,-1,0,0,(0,0,0))
457 	x = 0
458 	y = 0
459 	w = 1
460 	h = 1
461 	rx = prevProp[4][0]
462 	ry = prevProp[4][1]
463 	r = prevProp[4][2]
464 	if not values == '':
465 		for value in values.split(","):
466 			colon = value.find(":")
467 			if value[:colon] == 'x':
468 				x = float(value[colon + 1:])
469 			elif value[:colon] == 'y':
470 				y = float(value[colon + 1:])
471 			elif value[:colon] == 'w':
472 				w = float(value[colon + 1:])
473 			elif value[:colon] == 'h':
474 				h = float(value[colon + 1:])
475 			elif value[:colon] == 'r':
476 				r = float(value[colon + 1:])
477 			elif value[:colon] == 'rx':
478 				rx = float(value[colon + 1:])
479 			elif value[:colon] == 'ry':
480 				ry = float(value[colon + 1:])
481 			else:
482 				pass #the value does not contain relative information
483 	newRotation = (rx,ry,r)
484 	if newRow:
485 		x = rx + x
486 		if newRotation != prevProp[4]:
487 			y = ry + y
488 		else:
489 			y = prevProp[1] + 1 + y
490 	else:
491 		x = x + prevProp[0] + prevProp[2]
492 		y = prevProp[1]
493 
494 	props.append((x,y,w,h,newRotation))
495 
496 def modifyLabels(): #adds or removes label parameters based on the value of rotateSwitches and includeCutouts
497 	global labels
498 	result = []
499 	for label in labels:
500 		newLabel = label
501 		if includeCutOuts:
502 			if "!c!" in label:
503 				c = newLabel.index("!c!")
504 				newLabel = newLabel[:c] + newLabel[c+3:]
505 			else:
506 				newLabel = newLabel + "!c!"
507 		if rotateSwitches:
508 			if "!r!" in label:
509 				r = newLabel.index("!r!")
510 				newLabel = newLabel[:r] + newLabel[r+3:]
511 			else:
512 				newLabel = newLabel + "!r!"
513 		result.append(newLabel)
514 	labels = result
515 
516 def fixRotations(): #changes rotation data to actual coordinates from keyunit values
517 	global props
518 	result = []
519 	for prop in props:
520 		if prop[4][2] == 0:
521 			newRotation = False
522 		else:
523 			newRotation = (prop[4][0]*KEYUNIT, -prop[4][1]*KEYUNIT, -prop[4][2])
524 		result.append((prop[0], prop[1], prop[2], prop[3], newRotation))
525 	props = result
526 
527 #Output file methods
528 def save(saveAttempt=1):
529 	global doc
530 	global fileName
531 	if fileName[-6:] != ".FCStd":
532 		fileName = fileName + ".FCStd"
533 	saveAs = savePath + fileName
534 	if os.path.exists(saveAs):
535 		if saveAttempt == 1:
536 			fileName = fileName[:-6] + "(2).FCStd"
537 		else:
538 			i = fileName.find("("+ str(saveAttempt) +")")
539 			fileName = "{}({}).FCStd".format(fileName[:i], str(saveAttempt + 1))
540 		print("File already exists. Saving as " + fileName)
541 		save(saveAttempt + 1)
542 	else:
543 		doc.saveAs(saveAs)
544 		print("Successfully saved to " + saveAs)
545 
546 #global variables
547 doc = None
548 sketchCount = 0
549 props = [] #tuple for each switch, (x,y,w,h,(rx,ry,r))
550 labels = []	#labels for each switch from the layout editor
551 #################
552 import sys
553 import os
554 import math
555 if FREECADPATH and FREECADPATH2:
556 	sys.path.append(FREECADPATH)
557 	sys.path.append(FREECADPATH2)
558 ################
559 try:
560 	import FreeCAD
561 	import Sketcher
562 except Exception:
563 	print("error finding FreeCAD")
564 else:
565 	main()