camlib.py 28 KB

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