camlib.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. import cairo
  2. #from string import *
  3. #from math import *
  4. #from random import *
  5. #from struct import *
  6. #import os
  7. #import sys
  8. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
  9. from matplotlib.figure import Figure
  10. # See: http://toblerity.org/shapely/manual.html
  11. from shapely.geometry import Polygon, LineString, Point
  12. from shapely.geometry import MultiPoint, MultiPolygon
  13. from shapely.geometry import box as shply_box
  14. from shapely.ops import cascaded_union
  15. from descartes.patch import PolygonPatch
  16. class Geometry:
  17. def __init__(self):
  18. # Units (in or mm)
  19. self.units = 'in'
  20. # Final geometry: MultiPolygon
  21. self.solid_geometry = None
  22. def isolation_geometry(self, offset):
  23. '''
  24. Creates contours around geometry at a given
  25. offset distance.
  26. '''
  27. return self.solid_geometry.buffer(offset)
  28. def bounds(self):
  29. '''
  30. Returns coordinates of rectangular bounds
  31. of geometry: (xmin, ymin, xmax, ymax).
  32. '''
  33. if self.solid_geometry == None:
  34. print "Warning: solid_geometry not computed yet."
  35. return (0,0,0,0)
  36. return self.solid_geometry.bounds
  37. def size(self):
  38. '''
  39. Returns (width, height) of rectangular
  40. bounds of geometry.
  41. '''
  42. if self.solid_geometry == None:
  43. print "Warning: solid_geometry not computed yet."
  44. return 0
  45. bounds = self.bounds()
  46. return (bounds[2]-bounds[0], bounds[3]-bounds[1])
  47. def get_empty_area(self, boundary=None):
  48. '''
  49. Returns the complement of self.solid_geometry within
  50. the given boundary polygon. If not specified, it defaults to
  51. the rectangular bounding box of self.solid_geometry.
  52. '''
  53. if boundary == None:
  54. boundary = self.solid_geometry.envelope
  55. return boundary.difference(g.solid_geometry)
  56. def clear_polygon(self, polygon, tooldia, overlap = 0.15):
  57. '''
  58. Creates geometry inside a polygon for a tool to cover
  59. the whole area.
  60. '''
  61. poly_cuts = [polygon.buffer(-tooldia/2.0)]
  62. while(1):
  63. polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  64. if polygon.area > 0:
  65. poly_cuts.append(polygon)
  66. else:
  67. break
  68. return poly_cuts
  69. class Gerber (Geometry):
  70. def __init__(self):
  71. # Initialize parent
  72. Geometry.__init__(self)
  73. # Number format
  74. self.digits = 3
  75. self.fraction = 4
  76. ## Gerber elements ##
  77. # Apertures {'id':{'type':chr,
  78. # ['size':float], ['width':float],
  79. # ['height':float]}, ...}
  80. self.apertures = {}
  81. # Paths [{'linestring':LineString, 'aperture':dict}]
  82. self.paths = []
  83. # Buffered Paths [Polygon]
  84. # Paths transformed into Polygons by
  85. # offsetting the aperture size/2
  86. self.buffered_paths = []
  87. # Polygon regions [{'polygon':Polygon, 'aperture':dict}]
  88. self.regions = []
  89. # Flashes [{'loc':[float,float], 'aperture':dict}]
  90. self.flashes = []
  91. # Geometry from flashes
  92. self.flash_geometry = []
  93. def fix_regions(self):
  94. '''
  95. Overwrites the region polygons with fixed
  96. versions if found to be invalid (according to Shapely).
  97. '''
  98. for region in self.regions:
  99. if region['polygon'].is_valid == False:
  100. region['polygon'] = region['polygon'].buffer(0)
  101. def buffer_paths(self):
  102. self.buffered_paths = []
  103. for path in self.paths:
  104. width = self.apertures[path["aperture"]]["size"]
  105. self.buffered_paths.append(path["linestring"].buffer(width/2))
  106. def aperture_parse(self, gline):
  107. '''
  108. Parse gerber aperture definition
  109. into dictionary of apertures.
  110. '''
  111. indexstar = gline.find("*")
  112. indexC = gline.find("C,")
  113. if indexC != -1: # Circle, example: %ADD11C,0.1*%
  114. apid = gline[4:indexC]
  115. self.apertures[apid] = {"type":"C",
  116. "size":float(gline[indexC+2:indexstar])}
  117. return apid
  118. indexR = gline.find("R,")
  119. if indexR != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
  120. apid = gline[4:indexR]
  121. indexX = gline.find("X")
  122. self.apertures[apid] = {"type":"R",
  123. "width":float(gline[indexR+2:indexX]),
  124. "height":float(gline[indexX+1:indexstar])}
  125. return apid
  126. indexO = gline.find("O,")
  127. if indexO != -1: # Obround
  128. apid = gline[4:indexO]
  129. indexX = gline.find("X")
  130. self.apertures[apid] = {"type":"O",
  131. "width":float(gline[indexO+2:indexX]),
  132. "height":float(gline[indexX+1:indexstar])}
  133. return apid
  134. print "WARNING: Aperture not implemented:", gline
  135. return None
  136. def parse_file(self, filename):
  137. '''
  138. Calls Gerber.parse_lines() with array of lines
  139. read from the given file.
  140. '''
  141. gfile = open(filename, 'r')
  142. gstr = gfile.readlines()
  143. gfile.close()
  144. self.parse_lines(gstr)
  145. def parse_lines(self, glines):
  146. '''
  147. Main Gerber parser.
  148. '''
  149. path = [] # Coordinates of the current path
  150. last_path_aperture = None
  151. current_aperture = None
  152. for gline in glines:
  153. if gline.find("D01*") != -1: # pen down
  154. path.append(coord(gline, self.digits, self.fraction))
  155. last_path_aperture = current_aperture
  156. continue
  157. if gline.find("D02*") != -1: # pen up
  158. if len(path) > 1:
  159. # Path completed, create shapely LineString
  160. self.paths.append({"linestring":LineString(path),
  161. "aperture":last_path_aperture})
  162. path = [coord(gline, self.digits, self.fraction)]
  163. continue
  164. indexD3 = gline.find("D03*")
  165. if indexD3 > 0: # Flash
  166. self.flashes.append({"loc":coord(gline, self.digits, self.fraction),
  167. "aperture":current_aperture})
  168. continue
  169. if indexD3 == 0: # Flash?
  170. print "WARNING: Uninplemented flash style:", gline
  171. continue
  172. if gline.find("G37*") != -1: # end region
  173. # Only one path defines region?
  174. self.regions.append({"polygon":Polygon(path),
  175. "aperture":last_path_aperture})
  176. path = []
  177. continue
  178. if gline.find("%ADD") != -1: # aperture definition
  179. self.aperture_parse(gline) # adds element to apertures
  180. continue
  181. indexstar = gline.find("*")
  182. if gline.find("D") == 0: # Aperture change
  183. current_aperture = gline[1:indexstar]
  184. continue
  185. if gline.find("G54D") == 0: # Aperture change (deprecated)
  186. current_aperture = gline[4:indexstar]
  187. continue
  188. if gline.find("%FS") != -1: # Format statement
  189. indexX = gline.find("X")
  190. self.digits = int(gline[indexX + 1])
  191. self.fraction = int(gline[indexX + 2])
  192. continue
  193. print "WARNING: Line ignored:", gline
  194. if len(path) > 1:
  195. # EOF, create shapely LineString if something in path
  196. self.paths.append({"linestring":LineString(path),
  197. "aperture":last_path_aperture})
  198. def do_flashes(self):
  199. '''
  200. Creates geometry for Gerber flashes (aperture on a single point).
  201. '''
  202. self.flash_geometry = []
  203. for flash in self.flashes:
  204. aperture = self.apertures[flash['aperture']]
  205. if aperture['type'] == 'C': # Circles
  206. circle = Point(flash['loc']).buffer(aperture['size']/2)
  207. self.flash_geometry.append(circle)
  208. continue
  209. if aperture['type'] == 'R': # Rectangles
  210. loc = flash['loc']
  211. width = aperture['width']
  212. height = aperture['height']
  213. minx = loc[0] - width/2
  214. maxx = loc[0] + width/2
  215. miny = loc[1] - height/2
  216. maxy = loc[1] + height/2
  217. rectangle = shply_box(minx, miny, maxx, maxy)
  218. self.flash_geometry.append(rectangle)
  219. continue
  220. #TODO: Add support for type='O'
  221. print "WARNING: Aperture type %s not implemented"%(aperture['type'])
  222. def create_geometry(self):
  223. if len(self.buffered_paths) == 0:
  224. self.buffer_paths()
  225. self.fix_regions()
  226. self.do_flashes()
  227. self.solid_geometry = cascaded_union(
  228. self.buffered_paths +
  229. [poly['polygon'] for poly in self.regions] +
  230. self.flash_geometry)
  231. class Excellon(Geometry):
  232. def __init__(self):
  233. Geometry.__init__(self)
  234. self.tools = {}
  235. self.drills = []
  236. def parse_file(self, filename):
  237. efile = open(filename, 'r')
  238. estr = efile.readlines()
  239. efile.close()
  240. self.parse_lines(estr)
  241. def parse_lines(self, elines):
  242. '''
  243. Main Excellon parser.
  244. '''
  245. current_tool = ""
  246. for eline in elines:
  247. ## Tool definitions ##
  248. # TODO: Verify all this
  249. indexT = eline.find("T")
  250. indexC = eline.find("C")
  251. indexF = eline.find("F")
  252. # Type 1
  253. if indexT != -1 and indexC > indexT and indexF > indexF:
  254. tool = eline[1:indexC]
  255. spec = eline[indexC+1:indexF]
  256. self.tools[tool] = spec
  257. continue
  258. # Type 2
  259. # TODO: Is this inches?
  260. #indexsp = eline.find(" ")
  261. #indexin = eline.find("in")
  262. #if indexT != -1 and indexsp > indexT and indexin > indexsp:
  263. # tool = eline[1:indexsp]
  264. # spec = eline[indexsp+1:indexin]
  265. # self.tools[tool] = spec
  266. # continue
  267. # Type 3
  268. if indexT != -1 and indexC > indexT:
  269. tool = eline[1:indexC]
  270. spec = eline[indexC+1:-1]
  271. self.tools[tool] = spec
  272. continue
  273. ## Tool change
  274. if indexT == 0:
  275. current_tool = eline[1:-1]
  276. continue
  277. ## Drill
  278. indexX = eline.find("X")
  279. indexY = eline.find("Y")
  280. if indexX != -1 and indexY != -1:
  281. x = float(int(eline[indexX+1:indexY])/10000.0)
  282. y = float(int(eline[indexY+1:-1])/10000.0)
  283. self.drills.append({'point':Point((x,y)), 'tool':current_tool})
  284. continue
  285. print "WARNING: Line ignored:", eline
  286. def create_geometry(self):
  287. self.solid_geometry = []
  288. sizes = {}
  289. for tool in self.tools:
  290. sizes[tool] = float(self.tools[tool])
  291. for drill in self.drills:
  292. poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0)
  293. self.solid_geometry.append(poly)
  294. self.solid_geometry = cascaded_union(self.solid_geometry)
  295. class CNCjob:
  296. def __init__(self, units="in", kind="generic", z_move = 0.1,
  297. feedrate = 3.0, z_cut = -0.002):
  298. # Options
  299. self.kind = kind
  300. self.units = units
  301. self.z_cut = z_cut
  302. self.z_move = z_move
  303. self.feedrate = feedrate
  304. # Constants
  305. self.unitcode = {"in": "G20", "mm": "G21"}
  306. self.pausecode = "G04 P1"
  307. self.feedminutecode = "G94"
  308. self.absolutecode = "G90"
  309. # Input/Output G-Code
  310. self.gcode = ""
  311. # Bounds of geometry given to CNCjob.generate_from_geometry()
  312. self.input_geometry_bounds = None
  313. # Tool diameter given to CNCjob.generate_from_geometry()
  314. self.tooldia = 0
  315. # Output generated by CNCjob.create_gcode_geometry()
  316. self.G_geometry = None
  317. def generate_from_excellon(self, exobj):
  318. '''
  319. Generates G-code for drilling from excellon text.
  320. self.gcode becomes a list, each element is a
  321. different job for each tool in the excellon code.
  322. '''
  323. self.kind = "drill"
  324. self.gcode = []
  325. t = "G00 X%.4fY%.4f\n"
  326. down = "G01 Z%.4f\n"%self.z_cut
  327. up = "G01 Z%.4f\n"%self.z_move
  328. for tool in exobj.tools:
  329. points = []
  330. gcode = ""
  331. for drill in exobj.drill:
  332. if drill['tool'] == tool:
  333. points.append(drill['point'])
  334. gcode = self.unitcode[self.units] + "\n"
  335. gcode += self.absolutecode + "\n"
  336. gcode += self.feedminutecode + "\n"
  337. gcode += "F%.2f\n"%self.feedrate
  338. gcode += "G00 Z%.4f\n"%self.z_move # Move to travel height
  339. gcode += "M03\n" # Spindle start
  340. gcode += self.pausecode + "\n"
  341. for point in points:
  342. gcode += t%point
  343. gcode += down + up
  344. gcode += t%(0,0)
  345. gcode += "M05\n" # Spindle stop
  346. self.gcode.append(gcode)
  347. def generate_from_geometry(self, geometry, append=True, tooldia=None):
  348. '''
  349. Generates G-Code for geometry (Shapely collection).
  350. '''
  351. if tooldia == None:
  352. tooldia = self.tooldia
  353. else:
  354. self.tooldia = tooldia
  355. self.input_geometry_bounds = geometry.bounds
  356. if append == False:
  357. self.gcode = ""
  358. t = "G0%d X%.4fY%.4f\n"
  359. self.gcode = self.unitcode[self.units] + "\n"
  360. self.gcode += self.absolutecode + "\n"
  361. self.gcode += self.feedminutecode + "\n"
  362. self.gcode += "F%.2f\n"%self.feedrate
  363. self.gcode += "G00 Z%.4f\n"%self.z_move # Move to travel height
  364. self.gcode += "M03\n" # Spindle start
  365. self.gcode += self.pausecode + "\n"
  366. for geo in geometry:
  367. if type(geo) == Polygon:
  368. path = list(geo.exterior.coords) # Polygon exterior
  369. self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
  370. self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
  371. for pt in path[1:]:
  372. self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
  373. self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
  374. for ints in geo.interiors: # Polygon interiors
  375. path = list(ints.coords)
  376. self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
  377. self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
  378. for pt in path[1:]:
  379. self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
  380. self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
  381. continue
  382. if type(geo) == LineString or type(geo) == LineRing:
  383. path = list(geo.coords)
  384. self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
  385. self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
  386. for pt in path[1:]:
  387. self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
  388. self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
  389. continue
  390. if type(geo) == Point:
  391. path = list(geo.coords)
  392. self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
  393. self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
  394. self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
  395. continue
  396. print "WARNING: G-code generation not implemented for %s"%(str(type(geo)))
  397. self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
  398. self.gcode += "G00 X0Y0\n"
  399. self.gcode += "M05\n" # Spindle stop
  400. def create_gcode_geometry(self):
  401. steps_per_circ = 20
  402. '''
  403. G-Code parser (from self.gcode). Generates dictionary with
  404. single-segment LineString's and "kind" indicating cut or travel,
  405. fast or feedrate speed.
  406. '''
  407. geometry = []
  408. # TODO: ???? bring this into the class??
  409. gobjs = gparse1b(self.gcode)
  410. # Last known instruction
  411. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  412. # Process every instruction
  413. for gobj in gobjs:
  414. if 'Z' in gobj:
  415. if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  416. print "WARNING: Non-orthogonal motion: From", current
  417. print " To:", gobj
  418. current['Z'] = gobj['Z']
  419. if 'G' in gobj:
  420. current['G'] = int(gobj['G'])
  421. if 'X' in gobj or 'Y' in gobj:
  422. x = 0
  423. y = 0
  424. kind = ["C","F"] # T=travel, C=cut, F=fast, S=slow
  425. if 'X' in gobj:
  426. x = gobj['X']
  427. else:
  428. x = current['X']
  429. if 'Y' in gobj:
  430. y = gobj['Y']
  431. else:
  432. y = current['Y']
  433. if current['Z'] > 0:
  434. kind[0] = 'T'
  435. if current['G'] > 0:
  436. kind[1] = 'S'
  437. arcdir = [None, None, "cw", "ccw"]
  438. if current['G'] in [0,1]: # line
  439. geometry.append({'geom':LineString([(current['X'],current['Y']),
  440. (x,y)]), 'kind':kind})
  441. if current['G'] in [2,3]: # arc
  442. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  443. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  444. start = arctan2( -gobj['J'], -gobj['I'])
  445. stop = arctan2(-center[1]+y, -center[0]+x)
  446. geometry.append({'geom':arc(center, radius, start, stop,
  447. arcdir[current['G']],
  448. steps_per_circ),
  449. 'kind':kind})
  450. # Update current instruction
  451. for code in gobj:
  452. current[code] = gobj[code]
  453. self.G_geometry = geometry
  454. return geometry
  455. def plot(self, tooldia=None, dpi=75, margin=0.1,
  456. color={"T":["#F0E24D", "#B5AB3A"], "C":["#5E6CFF", "#4650BD"]},
  457. alpha={"T":0.3, "C":1.0}):
  458. '''
  459. Creates a Matplotlib figure with a plot of the
  460. G-code job.
  461. '''
  462. if tooldia == None:
  463. tooldia = self.tooldia
  464. fig = Figure(dpi=dpi)
  465. ax = fig.add_subplot(111)
  466. ax.set_aspect(1)
  467. xmin, ymin, xmax, ymax = self.input_geometry_bounds
  468. ax.set_xlim(xmin-margin, xmax+margin)
  469. ax.set_ylim(ymin-margin, ymax+margin)
  470. if tooldia == 0:
  471. for geo in self.G_geometry:
  472. linespec = '--'
  473. linecolor = color[geo['kind'][0]][1]
  474. if geo['kind'][0] == 'C':
  475. linespec = 'k-'
  476. x, y = geo['geom'].coords.xy
  477. ax.plot(x, y, linespec, color=linecolor)
  478. else:
  479. for geo in self.G_geometry:
  480. poly = geo['geom'].buffer(tooldia/2.0)
  481. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  482. edgecolor=color[geo['kind'][0]][1],
  483. alpha=alpha[geo['kind'][0]], zorder=2)
  484. ax.add_patch(patch)
  485. return fig
  486. def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
  487. color={"T":["#F0E24D", "#B5AB3A"], "C":["#5E6CFF", "#4650BD"]},
  488. alpha={"T":0.3, "C":1.0}):
  489. '''
  490. Plots the G-code job onto the given axes
  491. '''
  492. if tooldia == None:
  493. tooldia = self.tooldia
  494. #fig = Figure(dpi=dpi)
  495. #ax = fig.add_subplot(111)
  496. #ax.set_aspect(1)
  497. #xmin, ymin, xmax, ymax = self.input_geometry_bounds
  498. #ax.set_xlim(xmin-margin, xmax+margin)
  499. #ax.set_ylim(ymin-margin, ymax+margin)
  500. if tooldia == 0:
  501. for geo in self.G_geometry:
  502. linespec = '--'
  503. linecolor = color[geo['kind'][0]][1]
  504. if geo['kind'][0] == 'C':
  505. linespec = 'k-'
  506. x, y = geo['geom'].coords.xy
  507. axes.plot(x, y, linespec, color=linecolor)
  508. else:
  509. for geo in self.G_geometry:
  510. poly = geo['geom'].buffer(tooldia/2.0)
  511. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  512. edgecolor=color[geo['kind'][0]][1],
  513. alpha=alpha[geo['kind'][0]], zorder=2)
  514. axes.add_patch(patch)
  515. def gparse1b(gtext):
  516. '''
  517. gtext is a single string with g-code
  518. '''
  519. gcmds = []
  520. lines = gtext.split("\n") # TODO: This is probably a lot of work!
  521. for line in lines:
  522. line = line.strip()
  523. # Remove comments
  524. # NOTE: Limited to 1 bracket pair
  525. op = line.find("(")
  526. cl = line.find(")")
  527. if op > -1 and cl > op:
  528. #comment = line[op+1:cl]
  529. line = line[:op] + line[(cl+1):]
  530. # Parse GCode
  531. # 0 4 12
  532. # G01 X-0.007 Y-0.057
  533. # --> codes_idx = [0, 4, 12]
  534. codes = "NMGXYZIJFP"
  535. codes_idx = []
  536. i = 0
  537. for ch in line:
  538. if ch in codes:
  539. codes_idx.append(i)
  540. i += 1
  541. n_codes = len(codes_idx)
  542. if n_codes == 0:
  543. continue
  544. # Separate codes in line
  545. parts = []
  546. for p in range(n_codes-1):
  547. parts.append( line[ codes_idx[p]:codes_idx[p+1] ].strip() )
  548. parts.append( line[codes_idx[-1]:].strip() )
  549. # Separate codes from values
  550. cmds = {}
  551. for part in parts:
  552. cmds[part[0]] = float(part[1:])
  553. gcmds.append(cmds)
  554. return gcmds
  555. def get_bounds(geometry_sets):
  556. xmin = Inf
  557. ymin = Inf
  558. xmax = -Inf
  559. ymax = -Inf
  560. #geometry_sets = [self.gerbers, self.excellons]
  561. for gs in geometry_sets:
  562. for g in gs:
  563. gxmin, gymin, gxmax, gymax = g.solid_geometry.bounds
  564. xmin = min([xmin, gxmin])
  565. ymin = min([ymin, gymin])
  566. xmax = max([xmax, gxmax])
  567. ymax = max([ymax, gymax])
  568. return [xmin, ymin, xmax, ymax]
  569. def arc(center, radius, start, stop, direction, steps_per_circ):
  570. da_sign = {"cw":-1.0, "ccw":1.0}
  571. points = []
  572. if direction=="ccw" and stop <= start:
  573. stop += 2*pi
  574. if direction=="cw" and stop >= start:
  575. stop -= 2*pi
  576. angle = abs(stop - start)
  577. #angle = stop-start
  578. steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2])
  579. delta_angle = da_sign[direction]*angle*1.0/steps
  580. for i in range(steps+1):
  581. theta = start + delta_angle*i
  582. points.append([center[0]+radius*cos(theta), center[1]+radius*sin(theta)])
  583. return LineString(points)
  584. ############### cam.py ####################
  585. def coord(gstr,digits,fraction):
  586. '''
  587. Parse Gerber coordinates
  588. '''
  589. global gerbx, gerby
  590. xindex = gstr.find("X")
  591. yindex = gstr.find("Y")
  592. index = gstr.find("D")
  593. if (xindex == -1):
  594. x = gerbx
  595. y = int(gstr[(yindex+1):index])*(10**(-fraction))
  596. elif (yindex == -1):
  597. y = gerby
  598. x = int(gstr[(xindex+1):index])*(10**(-fraction))
  599. else:
  600. x = int(gstr[(xindex+1):yindex])*(10**(-fraction))
  601. y = int(gstr[(yindex+1):index])*(10**(-fraction))
  602. gerbx = x
  603. gerby = y
  604. return [x,y]
  605. ################ end of cam.py #############