camlib.py 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
  9. from matplotlib.figure import Figure
  10. import re
  11. # See: http://toblerity.org/shapely/manual.html
  12. from shapely.geometry import Polygon, LineString, Point, LinearRing
  13. from shapely.geometry import MultiPoint, MultiPolygon
  14. from shapely.geometry import box as shply_box
  15. from shapely.ops import cascaded_union
  16. import shapely.affinity as affinity
  17. from shapely.wkt import loads as sloads
  18. from shapely.wkt import dumps as sdumps
  19. from shapely.geometry.base import BaseGeometry
  20. # Used for solid polygons in Matplotlib
  21. from descartes.patch import PolygonPatch
  22. import simplejson as json
  23. # TODO: Commented for FlatCAM packaging with cx_freeze
  24. #from matplotlib.pyplot import plot
  25. class Geometry:
  26. def __init__(self):
  27. # Units (in or mm)
  28. self.units = 'in'
  29. # Final geometry: MultiPolygon
  30. self.solid_geometry = None
  31. # Attributes to be included in serialization
  32. self.ser_attrs = ['units', 'solid_geometry']
  33. def isolation_geometry(self, offset):
  34. """
  35. Creates contours around geometry at a given
  36. offset distance.
  37. :param offset: Offset distance.
  38. :type offset: float
  39. :return: The buffered geometry.
  40. :rtype: Shapely.MultiPolygon or Shapely.Polygon
  41. """
  42. return self.solid_geometry.buffer(offset)
  43. def bounds(self):
  44. """
  45. Returns coordinates of rectangular bounds
  46. of geometry: (xmin, ymin, xmax, ymax).
  47. """
  48. if self.solid_geometry is None:
  49. print "Warning: solid_geometry not computed yet."
  50. return (0, 0, 0, 0)
  51. if type(self.solid_geometry) == list:
  52. # TODO: This can be done faster. See comment from Shapely mailing lists.
  53. return cascaded_union(self.solid_geometry).bounds
  54. else:
  55. return self.solid_geometry.bounds
  56. def size(self):
  57. """
  58. Returns (width, height) of rectangular
  59. bounds of geometry.
  60. """
  61. if self.solid_geometry is None:
  62. print "Warning: solid_geometry not computed yet."
  63. return 0
  64. bounds = self.bounds()
  65. return (bounds[2]-bounds[0], bounds[3]-bounds[1])
  66. def get_empty_area(self, boundary=None):
  67. """
  68. Returns the complement of self.solid_geometry within
  69. the given boundary polygon. If not specified, it defaults to
  70. the rectangular bounding box of self.solid_geometry.
  71. """
  72. if boundary is None:
  73. boundary = self.solid_geometry.envelope
  74. return boundary.difference(self.solid_geometry)
  75. def clear_polygon(self, polygon, tooldia, overlap=0.15):
  76. """
  77. Creates geometry inside a polygon for a tool to cover
  78. the whole area.
  79. """
  80. poly_cuts = [polygon.buffer(-tooldia/2.0)]
  81. while True:
  82. polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  83. if polygon.area > 0:
  84. poly_cuts.append(polygon)
  85. else:
  86. break
  87. return poly_cuts
  88. def scale(self, factor):
  89. """
  90. Scales all of the object's geometry by a given factor. Override
  91. this method.
  92. :param factor: Number by which to scale.
  93. :type factor: float
  94. :return: None
  95. :rtype: None
  96. """
  97. return
  98. def offset(self, vect):
  99. """
  100. Offset the geometry by the given vector. Override this method.
  101. :param vect: (x, y) vector by which to offset the object.
  102. :type vect: tuple
  103. :return: None
  104. """
  105. return
  106. def convert_units(self, units):
  107. """
  108. Converts the units of the object to ``units`` by scaling all
  109. the geometry appropriately. This call ``scale()``. Don't call
  110. it again in descendents.
  111. :param units: "IN" or "MM"
  112. :type units: str
  113. :return: Scaling factor resulting from unit change.
  114. :rtype: float
  115. """
  116. print "Geometry.convert_units()"
  117. if units.upper() == self.units.upper():
  118. return 1.0
  119. if units.upper() == "MM":
  120. factor = 25.4
  121. elif units.upper() == "IN":
  122. factor = 1/25.4
  123. else:
  124. print "Unsupported units:", units
  125. return 1.0
  126. self.units = units
  127. self.scale(factor)
  128. return factor
  129. def to_dict(self):
  130. """
  131. Returns a respresentation of the object as a dictionary.
  132. Attributes to include are listed in ``self.ser_attrs``.
  133. :return: A dictionary-encoded copy of the object.
  134. :rtype: dict
  135. """
  136. d = {}
  137. for attr in self.ser_attrs:
  138. d[attr] = getattr(self, attr)
  139. return d
  140. def from_dict(self, d):
  141. """
  142. Sets object's attributes from a dictionary.
  143. Attributes to include are listed in ``self.ser_attrs``.
  144. """
  145. for attr in self.ser_attrs:
  146. setattr(self, attr, d[attr])
  147. class Gerber (Geometry):
  148. """
  149. **ATTRIBUTES**
  150. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  151. The values are dictionaries key/value pairs which describe the aperture. The
  152. type key is always present and the rest depend on the key:
  153. +-----------+-----------------------------------+
  154. | Key | Value |
  155. +===========+===================================+
  156. | type | (str) "C", "R", or "O" |
  157. +-----------+-----------------------------------+
  158. | others | Depend on ``type`` |
  159. +-----------+-----------------------------------+
  160. * ``paths`` (list): A path is described by a line an aperture that follows that
  161. line. Each paths[i] is a dictionary:
  162. +------------+------------------------------------------------+
  163. | Key | Value |
  164. +============+================================================+
  165. | linestring | (Shapely.LineString) The actual path. |
  166. +------------+------------------------------------------------+
  167. | aperture | (str) The key for an aperture in apertures. |
  168. +------------+------------------------------------------------+
  169. * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
  170. is a dictionary:
  171. +------------+------------------------------------------------+
  172. | Key | Value |
  173. +============+================================================+
  174. | loc | (list) [x (float), y (float)] coordinates. |
  175. +------------+------------------------------------------------+
  176. | aperture | (str) The key for an aperture in apertures. |
  177. +------------+------------------------------------------------+
  178. * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
  179. which have an exterior and zero or more interiors. An aperture is also
  180. associated with a region. Each is a dictionary:
  181. +------------+-----------------------------------------------------+
  182. | Key | Value |
  183. +============+=====================================================+
  184. | polygon | (Shapely.Polygon) The polygon defining the region. |
  185. +------------+-----------------------------------------------------+
  186. | aperture | (str) The key for an aperture in apertures. |
  187. +------------+-----------------------------------------------------+
  188. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  189. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  190. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  191. *buffering* (or thickening) the ``paths`` with the aperture. These are
  192. generated from ``paths`` in ``buffer_paths()``.
  193. **USAGE**::
  194. g = Gerber()
  195. g.parse_file(filename)
  196. g.create_geometry()
  197. do_something(s.solid_geometry)
  198. """
  199. def __init__(self):
  200. """
  201. The constructor takes no parameters. Use ``gerber.parse_files()``
  202. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  203. :return: Gerber object
  204. :rtype: Gerber
  205. """
  206. # Initialize parent
  207. Geometry.__init__(self)
  208. # Number format
  209. self.int_digits = 3
  210. """Number of integer digits in Gerber numbers. Used during parsing."""
  211. self.frac_digits = 4
  212. """Number of fraction digits in Gerber numbers. Used during parsing."""
  213. ## Gerber elements ##
  214. # Apertures {'id':{'type':chr,
  215. # ['size':float], ['width':float],
  216. # ['height':float]}, ...}
  217. self.apertures = {}
  218. # Paths [{'linestring':LineString, 'aperture':str}]
  219. self.paths = []
  220. # Buffered Paths [Polygon]
  221. # Paths transformed into Polygons by
  222. # offsetting the aperture size/2
  223. self.buffered_paths = []
  224. # Polygon regions [{'polygon':Polygon, 'aperture':str}]
  225. self.regions = []
  226. # Flashes [{'loc':[float,float], 'aperture':str}]
  227. self.flashes = []
  228. # Geometry from flashes
  229. self.flash_geometry = []
  230. # Attributes to be included in serialization
  231. # Always append to it because it carries contents
  232. # from Geometry.
  233. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
  234. 'buffered_paths', 'regions', 'flashes',
  235. 'flash_geometry']
  236. #### Parser patterns ####
  237. # FS - Format Specification
  238. # The format of X and Y must be the same!
  239. # L-omit leading zeros, T-omit trailing zeros
  240. # A-absolute notation, I-incremental notation
  241. self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
  242. # Mode (IN/MM)
  243. self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
  244. # Comment G04|G4
  245. self.comm_re = re.compile(r'^G0?4(.*)$')
  246. # AD - Aperture definition
  247. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*),(.*)\*%$')
  248. # AM - Aperture Macro
  249. # Beginning of macro (Ends with *%):
  250. self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  251. # Tool change
  252. # May begin with G54 but that is deprecated
  253. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  254. # G01 - Linear interpolation plus flashes
  255. # Operation code (D0x) missing is deprecated... oh well I will support it.
  256. self.lin_re = re.compile(r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$')
  257. self.setlin_re = re.compile(r'^(?:G0?1)\*')
  258. # G02/3 - Circular interpolation
  259. # 2-clockwise, 3-counterclockwise
  260. self.circ_re = re.compile(r'^(?:G0?([23]))?(?:X(-?\d+))?(?:Y(-?\d+))' +
  261. '?(?:I(-?\d+))?(?:J(-?\d+))?D0([12])\*$')
  262. # G01/2/3 Occurring without coordinates
  263. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  264. # Single D74 or multi D75 quadrant for circular interpolation
  265. self.quad_re = re.compile(r'^G7([45])\*$')
  266. # Region mode on
  267. # In region mode, D01 starts a region
  268. # and D02 ends it. A new region can be started again
  269. # with D01. All contours must be closed before
  270. # D02 or G37.
  271. self.regionon_re = re.compile(r'^G36\*$')
  272. # Region mode off
  273. # Will end a region and come off region mode.
  274. # All contours must be closed before D02 or G37.
  275. self.regionoff_re = re.compile(r'^G37\*$')
  276. # End of file
  277. self.eof_re = re.compile(r'^M02\*')
  278. # IP - Image polarity
  279. self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
  280. # LP - Level polarity
  281. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  282. # TODO: This is bad.
  283. self.steps_per_circ = 40
  284. def scale(self, factor):
  285. """
  286. Scales the objects' geometry on the XY plane by a given factor.
  287. These are:
  288. * ``apertures``
  289. * ``paths``
  290. * ``regions``
  291. * ``flashes``
  292. Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
  293. are re-created with ``self.create_geometry()``.
  294. :param factor: Number by which to scale.
  295. :type factor: float
  296. :rtype : None
  297. """
  298. # Apertures
  299. print "Scaling apertures..."
  300. for apid in self.apertures:
  301. for param in self.apertures[apid]:
  302. if param != "type": # All others are dimensions.
  303. print "Tool:", apid, "Parameter:", param
  304. self.apertures[apid][param] *= factor
  305. # Paths
  306. print "Scaling paths..."
  307. for path in self.paths:
  308. path['linestring'] = affinity.scale(path['linestring'],
  309. factor, factor, origin=(0, 0))
  310. # Flashes
  311. print "Scaling flashes..."
  312. for fl in self.flashes:
  313. # TODO: Shouldn't 'loc' be a numpy.array()?
  314. fl['loc'][0] *= factor
  315. fl['loc'][1] *= factor
  316. # Regions
  317. print "Scaling regions..."
  318. for reg in self.regions:
  319. reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
  320. origin=(0, 0))
  321. # Now buffered_paths, flash_geometry and solid_geometry
  322. self.create_geometry()
  323. def offset(self, vect):
  324. """
  325. Offsets the objects' geometry on the XY plane by a given vector.
  326. These are:
  327. * ``paths``
  328. * ``regions``
  329. * ``flashes``
  330. Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
  331. are re-created with ``self.create_geometry()``.
  332. :param vect: (x, y) offset vector.
  333. :type vect: tuple
  334. :return: None
  335. """
  336. dx, dy = vect
  337. # Paths
  338. print "Shifting paths..."
  339. for path in self.paths:
  340. path['linestring'] = affinity.translate(path['linestring'],
  341. xoff=dx, yoff=dy)
  342. # Flashes
  343. print "Shifting flashes..."
  344. for fl in self.flashes:
  345. # TODO: Shouldn't 'loc' be a numpy.array()?
  346. fl['loc'][0] += dx
  347. fl['loc'][1] += dy
  348. # Regions
  349. print "Shifting regions..."
  350. for reg in self.regions:
  351. reg['polygon'] = affinity.translate(reg['polygon'],
  352. xoff=dx, yoff=dy)
  353. # Now buffered_paths, flash_geometry and solid_geometry
  354. self.create_geometry()
  355. def fix_regions(self):
  356. """
  357. Overwrites the region polygons with fixed
  358. versions if found to be invalid (according to Shapely).
  359. """
  360. for region in self.regions:
  361. if not region['polygon'].is_valid:
  362. region['polygon'] = region['polygon'].buffer(0)
  363. def buffer_paths(self):
  364. """
  365. This is part of the parsing process. "Thickens" the paths
  366. by their appertures. This will only work for circular appertures.
  367. :return: None
  368. """
  369. self.buffered_paths = []
  370. for path in self.paths:
  371. try:
  372. width = self.apertures[path["aperture"]]["size"]
  373. self.buffered_paths.append(path["linestring"].buffer(width/2))
  374. except KeyError:
  375. print "ERROR: Failed to buffer path: ", path
  376. print "Apertures: ", self.apertures
  377. def aperture_parse(self, gline):
  378. """
  379. Parse gerber aperture definition into dictionary of apertures.
  380. The following kinds and their attributes are supported:
  381. * *Circular (C)*: size (float)
  382. * *Rectangle (R)*: width (float), height (float)
  383. * *Obround (O)*: width (float), height (float).
  384. :param gline: Line of Gerber code known to have an aperture definition.
  385. :type gline: str
  386. :return: Identifier of the aperture.
  387. :rtype: str
  388. """
  389. indexstar = gline.find("*")
  390. indexc = gline.find("C,")
  391. if indexc != -1: # Circle, example: %ADD11C,0.1*%
  392. # Found some Gerber with a leading zero in the aperture id and the
  393. # referenced it without the zero, so this is a hack to handle that.
  394. apid = str(int(gline[4:indexc]))
  395. self.apertures[apid] = {"type": "C",
  396. "size": float(gline[indexc+2:indexstar])}
  397. return apid
  398. indexr = gline.find("R,")
  399. if indexr != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
  400. # Hack explained above
  401. apid = str(int(gline[4:indexr]))
  402. indexx = gline.find("X")
  403. self.apertures[apid] = {"type": "R",
  404. "width": float(gline[indexr+2:indexx]),
  405. "height": float(gline[indexx+1:indexstar])}
  406. return apid
  407. indexo = gline.find("O,")
  408. if indexo != -1: # Obround
  409. # Hack explained above
  410. apid = str(int(gline[4:indexo]))
  411. indexx = gline.find("X")
  412. self.apertures[apid] = {"type": "O",
  413. "width": float(gline[indexo+2:indexx]),
  414. "height": float(gline[indexx+1:indexstar])}
  415. return apid
  416. print "WARNING: Aperture not implemented:", gline
  417. return None
  418. def parse_file(self, filename):
  419. """
  420. Calls Gerber.parse_lines() with array of lines
  421. read from the given file.
  422. """
  423. gfile = open(filename, 'r')
  424. gstr = gfile.readlines()
  425. gfile.close()
  426. self.parse_lines(gstr)
  427. def parse_lines(self, glines):
  428. """
  429. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  430. ``self.flashes``, ``self.regions`` and ``self.units``.
  431. :param glines: Gerber code as list of strings, each
  432. element being one line of the source file.
  433. :type glines: list
  434. :return: None
  435. :rtype: None
  436. """
  437. path = [] # Coordinates of the current path, each is [x, y]
  438. last_path_aperture = None
  439. current_aperture = None
  440. # 1,2 or 3 from "G01", "G02" or "G03"
  441. current_interpolation_mode = None
  442. # 1 or 2 from "D01" or "D02"
  443. # Note this is to support deprecated Gerber not putting
  444. # an operation code at the end of every coordinate line.
  445. current_operation_code = None
  446. # Current coordinates
  447. current_x = None
  448. current_y = None
  449. # How to interprest circular interpolation: SINGLE or MULTI
  450. quadrant_mode = None
  451. line_num = 0
  452. for gline in glines:
  453. line_num += 1
  454. ## G01 - Linear interpolation plus flashes
  455. # Operation code (D0x) missing is deprecated... oh well I will support it.
  456. match = self.lin_re.search(gline)
  457. if match:
  458. # Dxx alone? Will ignore for now.
  459. if match.group(1) is None and match.group(2) is None and match.group(3) is None:
  460. try:
  461. current_operation_code = int(match.group(4))
  462. except:
  463. pass # A line with just * will match too.
  464. continue
  465. # Parse coordinates
  466. if match.group(2) is not None:
  467. current_x = parse_gerber_number(match.group(2), self.frac_digits)
  468. if match.group(3) is not None:
  469. current_y = parse_gerber_number(match.group(3), self.frac_digits)
  470. # Parse operation code
  471. if match.group(4) is not None:
  472. current_operation_code = int(match.group(4))
  473. # Pen down: add segment
  474. if current_operation_code == 1:
  475. path.append([current_x, current_y])
  476. last_path_aperture = current_aperture
  477. # Pen up: finish path
  478. elif current_operation_code == 2:
  479. if len(path) > 1:
  480. if last_path_aperture is None:
  481. print "Warning: No aperture defined for curent path. (%d)" % line_num
  482. self.paths.append({"linestring": LineString(path),
  483. "aperture": last_path_aperture})
  484. path = [[current_x, current_y]] # Start new path
  485. # Flash
  486. elif current_operation_code == 3:
  487. self.flashes.append({"loc": [current_x, current_y],
  488. "aperture": current_aperture})
  489. continue
  490. ## G02/3 - Circular interpolation
  491. # 2-clockwise, 3-counterclockwise
  492. match = self.circ_re.search(gline)
  493. if match:
  494. mode, x, y, i, j, d = match.groups()
  495. try:
  496. x = parse_gerber_number(x, self.frac_digits)
  497. except:
  498. x = current_x
  499. try:
  500. y = parse_gerber_number(y, self.frac_digits)
  501. except:
  502. y = current_y
  503. try:
  504. i = parse_gerber_number(i, self.frac_digits)
  505. except:
  506. i = 0
  507. try:
  508. j = parse_gerber_number(j, self.frac_digits)
  509. except:
  510. j = 0
  511. if quadrant_mode is None:
  512. print "ERROR: Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num
  513. print gline
  514. continue
  515. if mode is None and current_interpolation_mode not in [2, 3]:
  516. print "ERROR: Found arc without circular interpolation mode defined. (%d)" % line_num
  517. print gline
  518. continue
  519. elif mode is not None:
  520. current_interpolation_mode = int(mode)
  521. # Set operation code if provided
  522. if d is not None:
  523. current_operation_code = int(d)
  524. # Nothing created! Pen Up.
  525. if current_operation_code == 2:
  526. print "Warning: Arc with D2. (%d)" % line_num
  527. if len(path) > 1:
  528. if last_path_aperture is None:
  529. print "Warning: No aperture defined for curent path. (%d)" % line_num
  530. self.paths.append({"linestring": LineString(path),
  531. "aperture": last_path_aperture})
  532. current_x = x
  533. current_y = y
  534. path = [[current_x, current_y]] # Start new path
  535. continue
  536. # Flash should not happen here
  537. if current_operation_code == 3:
  538. print "ERROR: Trying to flash within arc. (%d)" % line_num
  539. continue
  540. if quadrant_mode == 'MULTI':
  541. center = [i + current_x, j + current_y]
  542. radius = sqrt(i**2 + j**2)
  543. start = arctan2(-j, -i)
  544. stop = arctan2(-center[1] + y, -center[0] + x)
  545. arcdir = [None, None, "cw", "ccw"]
  546. this_arc = arc(center, radius, start, stop,
  547. arcdir[current_interpolation_mode],
  548. self.steps_per_circ)
  549. # Last point in path is current point
  550. current_x = this_arc[-1][0]
  551. current_y = this_arc[-1][1]
  552. # Append
  553. path += this_arc
  554. last_path_aperture = current_aperture
  555. continue
  556. if quadrant_mode == 'SINGLE':
  557. print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
  558. ## G74/75* - Single or multiple quadrant arcs
  559. match = self.quad_re.search(gline)
  560. if match:
  561. if match.group(1) == '4':
  562. quadrant_mode = 'SINGLE'
  563. else:
  564. quadrant_mode = 'MULTI'
  565. continue
  566. ## G37* - End region
  567. if self.regionoff_re.search(gline):
  568. # Only one path defines region?
  569. if len(path) < 3:
  570. print "ERROR: Path contains less than 3 points:"
  571. print path
  572. print "Line (%d): " % line_num, gline
  573. path = []
  574. continue
  575. # For regions we may ignore an aperture that is None
  576. self.regions.append({"polygon": Polygon(path),
  577. "aperture": last_path_aperture})
  578. #path = []
  579. path = [[current_x, current_y]] # Start new path
  580. continue
  581. if gline.find("%ADD") != -1: # aperture definition
  582. self.aperture_parse(gline) # adds element to apertures
  583. continue
  584. ## G01/2/3* - Interpolation mode change
  585. # Can occur along with coordinates and operation code but
  586. # sometimes by itself (handled here).
  587. # Example: G01*
  588. match = self.interp_re.search(gline)
  589. if match:
  590. current_interpolation_mode = int(match.group(1))
  591. continue
  592. ## Tool/aperture change
  593. # Example: D12*
  594. match = self.tool_re.search(gline)
  595. if match:
  596. current_aperture = match.group(1)
  597. continue
  598. ## Number format
  599. # Example: %FSLAX24Y24*%
  600. # TODO: This is ignoring most of the format. Implement the rest.
  601. match = self.fmt_re.search(gline)
  602. if match:
  603. self.int_digits = int(match.group(3))
  604. self.frac_digits = int(match.group(4))
  605. continue
  606. ## Mode (IN/MM)
  607. # Example: %MOIN*%
  608. match = self.mode_re.search(gline)
  609. if match:
  610. self.units = match.group(1)
  611. continue
  612. print "WARNING: Line ignored (%d):" % line_num, gline
  613. if len(path) > 1:
  614. # EOF, create shapely LineString if something still in path
  615. self.paths.append({"linestring": LineString(path),
  616. "aperture": last_path_aperture})
  617. def do_flashes(self):
  618. """
  619. Creates geometry for Gerber flashes (aperture on a single point).
  620. """
  621. self.flash_geometry = []
  622. for flash in self.flashes:
  623. aperture = self.apertures[flash['aperture']]
  624. if aperture['type'] == 'C': # Circles
  625. circle = Point(flash['loc']).buffer(aperture['size']/2)
  626. self.flash_geometry.append(circle)
  627. continue
  628. if aperture['type'] == 'R': # Rectangles
  629. loc = flash['loc']
  630. width = aperture['width']
  631. height = aperture['height']
  632. minx = loc[0] - width/2
  633. maxx = loc[0] + width/2
  634. miny = loc[1] - height/2
  635. maxy = loc[1] + height/2
  636. rectangle = shply_box(minx, miny, maxx, maxy)
  637. self.flash_geometry.append(rectangle)
  638. continue
  639. if aperture['type'] == 'O': # Obround
  640. loc = flash['loc']
  641. width = aperture['width']
  642. height = aperture['height']
  643. if width > height:
  644. p1 = Point(loc[0] + 0.5*(width-height), loc[1])
  645. p2 = Point(loc[0] - 0.5*(width-height), loc[1])
  646. c1 = p1.buffer(height*0.5)
  647. c2 = p2.buffer(height*0.5)
  648. else:
  649. p1 = Point(loc[0], loc[1] + 0.5*(height-width))
  650. p2 = Point(loc[0], loc[1] - 0.5*(height-width))
  651. c1 = p1.buffer(width*0.5)
  652. c2 = p2.buffer(width*0.5)
  653. obround = cascaded_union([c1, c2]).convex_hull
  654. self.flash_geometry.append(obround)
  655. continue
  656. print "WARNING: Aperture type %s not implemented" % (aperture['type'])
  657. def create_geometry(self):
  658. """
  659. Geometry from a Gerber file is made up entirely of polygons.
  660. Every stroke (linear or circular) has an aperture which gives
  661. it thickness. Additionally, aperture strokes have non-zero area,
  662. and regions naturally do as well.
  663. :rtype : None
  664. :return: None
  665. """
  666. # if len(self.buffered_paths) == 0:
  667. # self.buffer_paths()
  668. print "... buffer_paths()"
  669. self.buffer_paths()
  670. print "... fix_regions()"
  671. self.fix_regions()
  672. print "... do_flashes()"
  673. self.do_flashes()
  674. print "... cascaded_union()"
  675. self.solid_geometry = cascaded_union(self.buffered_paths +
  676. [poly['polygon'] for poly in self.regions] +
  677. self.flash_geometry)
  678. def get_bounding_box(self, margin=0.0, rounded=False):
  679. """
  680. Creates and returns a rectangular polygon bounding at a distance of
  681. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  682. can optionally have rounded corners of radius equal to margin.
  683. :param margin: Distance to enlarge the rectangular bounding
  684. box in both positive and negative, x and y axes.
  685. :type margin: float
  686. :param rounded: Wether or not to have rounded corners.
  687. :type rounded: bool
  688. :return: The bounding box.
  689. :rtype: Shapely.Polygon
  690. """
  691. bbox = self.solid_geometry.envelope.buffer(margin)
  692. if not rounded:
  693. bbox = bbox.envelope
  694. return bbox
  695. class Excellon(Geometry):
  696. """
  697. *ATTRIBUTES*
  698. * ``tools`` (dict): The key is the tool name and the value is
  699. the size (diameter).
  700. * ``drills`` (list): Each is a dictionary:
  701. ================ ====================================
  702. Key Value
  703. ================ ====================================
  704. point (Shapely.Point) Where to drill
  705. tool (str) A key in ``tools``
  706. ================ ====================================
  707. """
  708. def __init__(self):
  709. """
  710. The constructor takes no parameters.
  711. :return: Excellon object.
  712. :rtype: Excellon
  713. """
  714. Geometry.__init__(self)
  715. self.tools = {}
  716. self.drills = []
  717. # Trailing "T" or leading "L"
  718. self.zeros = ""
  719. # Attributes to be included in serialization
  720. # Always append to it because it carries contents
  721. # from Geometry.
  722. self.ser_attrs += ['tools', 'drills', 'zeros']
  723. #### Patterns ####
  724. # Regex basics:
  725. # ^ - beginning
  726. # $ - end
  727. # *: 0 or more, +: 1 or more, ?: 0 or 1
  728. # M48 - Beggining of Part Program Header
  729. self.hbegin_re = re.compile(r'^M48$')
  730. # M95 or % - End of Part Program Header
  731. # NOTE: % has different meaning in the body
  732. self.hend_re = re.compile(r'^(?:M95|%)$')
  733. # FMAT Excellon format
  734. self.fmat_re = re.compile(r'^FMAT,([12])$')
  735. # Number format and units
  736. # INCH uses 6 digits
  737. # METRIC uses 5/6
  738. self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
  739. # Tool definition/parameters (?= is look-ahead
  740. # NOTE: This might be an overkill!
  741. self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
  742. r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  743. r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  744. r'(?=.*Z(-?\d*\.?\d*))?[CFSBHT]')
  745. # Tool select
  746. # Can have additional data after tool number but
  747. # is ignored if present in the header.
  748. # Warning: This will match toolset_re too.
  749. self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
  750. # Comment
  751. self.comm_re = re.compile(r'^;(.*)$')
  752. # Absolute/Incremental G90/G91
  753. self.absinc_re = re.compile(r'^G9([01])$')
  754. # Modes of operation
  755. # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
  756. self.modes_re = re.compile(r'^G0([012345])')
  757. # Measuring mode
  758. # 1-metric, 2-inch
  759. self.meas_re = re.compile(r'^M7([12])$')
  760. # Coordinates
  761. self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
  762. self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
  763. # R - Repeat hole (# times, X offset, Y offset)
  764. self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(\d*\.?\d*))?(?:Y(\d*\.?\d*))?$')
  765. # Various stop/pause commands
  766. self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
  767. def parse_file(self, filename):
  768. """
  769. Reads the specified file as array of lines as
  770. passes it to ``parse_lines()``.
  771. :param filename: The file to be read and parsed.
  772. :type filename: str
  773. :return: None
  774. """
  775. efile = open(filename, 'r')
  776. estr = efile.readlines()
  777. efile.close()
  778. self.parse_lines(estr)
  779. def parse_lines(self, elines):
  780. """
  781. Main Excellon parser.
  782. :param elines: List of strings, each being a line of Excellon code.
  783. :type elines: list
  784. :return: None
  785. """
  786. current_tool = ""
  787. in_header = False
  788. for eline in elines:
  789. ## Header Begin/End ##
  790. if self.hbegin_re.search(eline):
  791. in_header = True
  792. continue
  793. if self.hend_re.search(eline):
  794. in_header = False
  795. continue
  796. #### Body ####
  797. if not in_header:
  798. ## Tool change ##
  799. match = self.toolsel_re.search(eline)
  800. if match:
  801. current_tool = str(int(match.group(1)))
  802. continue
  803. ## Drill ##
  804. indexx = eline.find("X")
  805. indexy = eline.find("Y")
  806. if indexx != -1 and indexy != -1:
  807. x = float(int(eline[indexx+1:indexy])/10000.0)
  808. y = float(int(eline[indexy+1:-1])/10000.0)
  809. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  810. continue
  811. #### Header ####
  812. if in_header:
  813. ## Tool definitions ##
  814. match = self.toolset_re.search(eline)
  815. if match:
  816. name = str(int(match.group(1)))
  817. spec = {
  818. "C": float(match.group(2)),
  819. # "F": float(match.group(3)),
  820. # "S": float(match.group(4)),
  821. # "B": float(match.group(5)),
  822. # "H": float(match.group(6)),
  823. # "Z": float(match.group(7))
  824. }
  825. self.tools[name] = spec
  826. continue
  827. ## Units and number format ##
  828. match = self.units_re.match(eline)
  829. if match:
  830. self.zeros = match.group(2) # "T" or "L"
  831. self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  832. continue
  833. print "WARNING: Line ignored:", eline
  834. def create_geometry(self):
  835. self.solid_geometry = []
  836. for drill in self.drills:
  837. poly = Point(drill['point']).buffer(self.tools[drill['tool']]["C"]/2.0)
  838. self.solid_geometry.append(poly)
  839. #self.solid_geometry = cascaded_union(self.solid_geometry)
  840. def scale(self, factor):
  841. """
  842. Scales geometry on the XY plane in the object by a given factor.
  843. Tool sizes, feedrates an Z-plane dimensions are untouched.
  844. :param factor: Number by which to scale the object.
  845. :type factor: float
  846. :return: None
  847. :rtype: NOne
  848. """
  849. # Drills
  850. for drill in self.drills:
  851. drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
  852. self.create_geometry()
  853. def offset(self, vect):
  854. """
  855. Offsets geometry on the XY plane in the object by a given vector.
  856. :param vect: (x, y) offset vector.
  857. :type vect: tuple
  858. :return: None
  859. """
  860. dx, dy = vect
  861. # Drills
  862. for drill in self.drills:
  863. drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
  864. self.create_geometry()
  865. def convert_units(self, units):
  866. factor = Geometry.convert_units(self, units)
  867. # Tools
  868. for tname in self.tools:
  869. self.tools[tname]["C"] *= factor
  870. self.create_geometry()
  871. return factor
  872. class CNCjob(Geometry):
  873. """
  874. Represents work to be done by a CNC machine.
  875. *ATTRIBUTES*
  876. * ``gcode_parsed`` (list): Each is a dictionary:
  877. ===================== =========================================
  878. Key Value
  879. ===================== =========================================
  880. geom (Shapely.LineString) Tool path (XY plane)
  881. kind (string) "AB", A is "T" (travel) or
  882. "C" (cut). B is "F" (fast) or "S" (slow).
  883. ===================== =========================================
  884. """
  885. def __init__(self, units="in", kind="generic", z_move=0.1,
  886. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  887. Geometry.__init__(self)
  888. self.kind = kind
  889. self.units = units
  890. self.z_cut = z_cut
  891. self.z_move = z_move
  892. self.feedrate = feedrate
  893. self.tooldia = tooldia
  894. self.unitcode = {"IN": "G20", "MM": "G21"}
  895. self.pausecode = "G04 P1"
  896. self.feedminutecode = "G94"
  897. self.absolutecode = "G90"
  898. self.gcode = ""
  899. self.input_geometry_bounds = None
  900. self.gcode_parsed = None
  901. self.steps_per_circ = 20 # Used when parsing G-code arcs
  902. # Attributes to be included in serialization
  903. # Always append to it because it carries contents
  904. # from Geometry.
  905. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia',
  906. 'gcode', 'input_geometry_bounds', 'gcode_parsed',
  907. 'steps_per_circ']
  908. def convert_units(self, units):
  909. factor = Geometry.convert_units(self, units)
  910. print "CNCjob.convert_units()"
  911. self.z_cut *= factor
  912. self.z_move *= factor
  913. self.feedrate *= factor
  914. self.tooldia *= factor
  915. return factor
  916. def generate_from_excellon(self, exobj):
  917. """
  918. Generates G-code for drilling from Excellon object.
  919. self.gcode becomes a list, each element is a
  920. different job for each tool in the excellon code.
  921. """
  922. self.kind = "drill"
  923. self.gcode = []
  924. t = "G00 X%.4fY%.4f\n"
  925. down = "G01 Z%.4f\n" % self.z_cut
  926. up = "G01 Z%.4f\n" % self.z_move
  927. for tool in exobj.tools:
  928. points = []
  929. for drill in exobj.drill:
  930. if drill['tool'] == tool:
  931. points.append(drill['point'])
  932. gcode = self.unitcode[self.units.upper()] + "\n"
  933. gcode += self.absolutecode + "\n"
  934. gcode += self.feedminutecode + "\n"
  935. gcode += "F%.2f\n" % self.feedrate
  936. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  937. gcode += "M03\n" # Spindle start
  938. gcode += self.pausecode + "\n"
  939. for point in points:
  940. gcode += t % point
  941. gcode += down + up
  942. gcode += t % (0, 0)
  943. gcode += "M05\n" # Spindle stop
  944. self.gcode.append(gcode)
  945. def generate_from_excellon_by_tool(self, exobj, tools="all"):
  946. """
  947. Creates gcode for this object from an Excellon object
  948. for the specified tools.
  949. :param exobj: Excellon object to process
  950. :type exobj: Excellon
  951. :param tools: Comma separated tool names
  952. :type: tools: str
  953. :return: None
  954. :rtype: None
  955. """
  956. print "Creating CNC Job from Excellon..."
  957. if tools == "all":
  958. tools = [tool for tool in exobj.tools]
  959. else:
  960. tools = [x.strip() for x in tools.split(",")]
  961. tools = filter(lambda y: y in exobj.tools, tools)
  962. print "Tools are:", tools
  963. points = []
  964. for drill in exobj.drills:
  965. if drill['tool'] in tools:
  966. points.append(drill['point'])
  967. print "Found %d drills." % len(points)
  968. #self.kind = "drill"
  969. self.gcode = []
  970. t = "G00 X%.4fY%.4f\n"
  971. down = "G01 Z%.4f\n" % self.z_cut
  972. up = "G01 Z%.4f\n" % self.z_move
  973. gcode = self.unitcode[self.units.upper()] + "\n"
  974. gcode += self.absolutecode + "\n"
  975. gcode += self.feedminutecode + "\n"
  976. gcode += "F%.2f\n" % self.feedrate
  977. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  978. gcode += "M03\n" # Spindle start
  979. gcode += self.pausecode + "\n"
  980. for point in points:
  981. x, y = point.coords.xy
  982. gcode += t % (x[0], y[0])
  983. gcode += down + up
  984. gcode += t % (0, 0)
  985. gcode += "M05\n" # Spindle stop
  986. self.gcode = gcode
  987. def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0):
  988. """
  989. Generates G-Code from a Geometry object. Stores in ``self.gcode``.
  990. :param geometry: Geometry defining the toolpath
  991. :type geometry: Geometry
  992. :param append: Wether to append to self.gcode or re-write it.
  993. :type append: bool
  994. :param tooldia: If given, sets the tooldia property but does
  995. not affect the process in any other way.
  996. :type tooldia: bool
  997. :param tolerance: All points in the simplified object will be within the
  998. tolerance distance of the original geometry.
  999. :return: None
  1000. :rtype: None
  1001. """
  1002. if tooldia is not None:
  1003. self.tooldia = tooldia
  1004. self.input_geometry_bounds = geometry.bounds()
  1005. if not append:
  1006. self.gcode = ""
  1007. self.gcode = self.unitcode[self.units.upper()] + "\n"
  1008. self.gcode += self.absolutecode + "\n"
  1009. self.gcode += self.feedminutecode + "\n"
  1010. self.gcode += "F%.2f\n" % self.feedrate
  1011. self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1012. self.gcode += "M03\n" # Spindle start
  1013. self.gcode += self.pausecode + "\n"
  1014. for geo in geometry.solid_geometry:
  1015. if type(geo) == Polygon:
  1016. self.gcode += self.polygon2gcode(geo, tolerance=tolerance)
  1017. continue
  1018. if type(geo) == LineString or type(geo) == LinearRing:
  1019. self.gcode += self.linear2gcode(geo, tolerance=tolerance)
  1020. continue
  1021. if type(geo) == Point:
  1022. self.gcode += self.point2gcode(geo)
  1023. continue
  1024. if type(geo) == MultiPolygon:
  1025. for poly in geo:
  1026. self.gcode += self.polygon2gcode(poly, tolerance=tolerance)
  1027. continue
  1028. print "WARNING: G-code generation not implemented for %s" % (str(type(geo)))
  1029. self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1030. self.gcode += "G00 X0Y0\n"
  1031. self.gcode += "M05\n" # Spindle stop
  1032. def pre_parse(self, gtext):
  1033. """
  1034. Separates parts of the G-Code text into a list of dictionaries.
  1035. Used by ``self.gcode_parse()``.
  1036. :param gtext: A single string with g-code
  1037. """
  1038. # Units: G20-inches, G21-mm
  1039. units_re = re.compile(r'^G2([01])')
  1040. # TODO: This has to be re-done
  1041. gcmds = []
  1042. lines = gtext.split("\n") # TODO: This is probably a lot of work!
  1043. for line in lines:
  1044. # Clean up
  1045. line = line.strip()
  1046. # Remove comments
  1047. # NOTE: Limited to 1 bracket pair
  1048. op = line.find("(")
  1049. cl = line.find(")")
  1050. if op > -1 and cl > op:
  1051. #comment = line[op+1:cl]
  1052. line = line[:op] + line[(cl+1):]
  1053. # Units
  1054. match = units_re.match(line)
  1055. if match:
  1056. self.units = {'0': "IN", '1': "MM"}[match.group(1)]
  1057. # Parse GCode
  1058. # 0 4 12
  1059. # G01 X-0.007 Y-0.057
  1060. # --> codes_idx = [0, 4, 12]
  1061. codes = "NMGXYZIJFP"
  1062. codes_idx = []
  1063. i = 0
  1064. for ch in line:
  1065. if ch in codes:
  1066. codes_idx.append(i)
  1067. i += 1
  1068. n_codes = len(codes_idx)
  1069. if n_codes == 0:
  1070. continue
  1071. # Separate codes in line
  1072. parts = []
  1073. for p in range(n_codes-1):
  1074. parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
  1075. parts.append(line[codes_idx[-1]:].strip())
  1076. # Separate codes from values
  1077. cmds = {}
  1078. for part in parts:
  1079. cmds[part[0]] = float(part[1:])
  1080. gcmds.append(cmds)
  1081. return gcmds
  1082. def gcode_parse(self):
  1083. """
  1084. G-Code parser (from self.gcode). Generates dictionary with
  1085. single-segment LineString's and "kind" indicating cut or travel,
  1086. fast or feedrate speed.
  1087. """
  1088. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  1089. # Results go here
  1090. geometry = []
  1091. # TODO: Merge into single parser?
  1092. gobjs = self.pre_parse(self.gcode)
  1093. # Last known instruction
  1094. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  1095. # Current path: temporary storage until tool is
  1096. # lifted or lowered.
  1097. path = []
  1098. # Process every instruction
  1099. for gobj in gobjs:
  1100. # Changing height:
  1101. if 'Z' in gobj:
  1102. if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  1103. print "WARNING: Non-orthogonal motion: From", current
  1104. print " To:", gobj
  1105. current['Z'] = gobj['Z']
  1106. # Store the path into geometry and reset path
  1107. if len(path) > 1:
  1108. geometry.append({"geom": LineString(path),
  1109. "kind": kind})
  1110. path = [path[-1]] # Start with the last point of last path.
  1111. if 'G' in gobj:
  1112. current['G'] = int(gobj['G'])
  1113. if 'X' in gobj or 'Y' in gobj:
  1114. if 'X' in gobj:
  1115. x = gobj['X']
  1116. else:
  1117. x = current['X']
  1118. if 'Y' in gobj:
  1119. y = gobj['Y']
  1120. else:
  1121. y = current['Y']
  1122. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  1123. if current['Z'] > 0:
  1124. kind[0] = 'T'
  1125. if current['G'] > 0:
  1126. kind[1] = 'S'
  1127. arcdir = [None, None, "cw", "ccw"]
  1128. if current['G'] in [0, 1]: # line
  1129. path.append((x, y))
  1130. if current['G'] in [2, 3]: # arc
  1131. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  1132. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  1133. start = arctan2(-gobj['J'], -gobj['I'])
  1134. stop = arctan2(-center[1]+y, -center[0]+x)
  1135. path += arc(center, radius, start, stop,
  1136. arcdir[current['G']],
  1137. self.steps_per_circ)
  1138. # Update current instruction
  1139. for code in gobj:
  1140. current[code] = gobj[code]
  1141. self.gcode_parsed = geometry
  1142. return geometry
  1143. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  1144. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  1145. # alpha={"T": 0.3, "C": 1.0}):
  1146. # """
  1147. # Creates a Matplotlib figure with a plot of the
  1148. # G-code job.
  1149. # """
  1150. # if tooldia is None:
  1151. # tooldia = self.tooldia
  1152. #
  1153. # fig = Figure(dpi=dpi)
  1154. # ax = fig.add_subplot(111)
  1155. # ax.set_aspect(1)
  1156. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  1157. # ax.set_xlim(xmin-margin, xmax+margin)
  1158. # ax.set_ylim(ymin-margin, ymax+margin)
  1159. #
  1160. # if tooldia == 0:
  1161. # for geo in self.gcode_parsed:
  1162. # linespec = '--'
  1163. # linecolor = color[geo['kind'][0]][1]
  1164. # if geo['kind'][0] == 'C':
  1165. # linespec = 'k-'
  1166. # x, y = geo['geom'].coords.xy
  1167. # ax.plot(x, y, linespec, color=linecolor)
  1168. # else:
  1169. # for geo in self.gcode_parsed:
  1170. # poly = geo['geom'].buffer(tooldia/2.0)
  1171. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  1172. # edgecolor=color[geo['kind'][0]][1],
  1173. # alpha=alpha[geo['kind'][0]], zorder=2)
  1174. # ax.add_patch(patch)
  1175. #
  1176. # return fig
  1177. def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
  1178. color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  1179. alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
  1180. """
  1181. Plots the G-code job onto the given axes.
  1182. :param axes: Matplotlib axes on which to plot.
  1183. :param tooldia: Tool diameter.
  1184. :param dpi: Not used!
  1185. :param margin: Not used!
  1186. :param color: Color specification.
  1187. :param alpha: Transparency specification.
  1188. :param tool_tolerance: Tolerance when drawing the toolshape.
  1189. :return: None
  1190. """
  1191. if tooldia is None:
  1192. tooldia = self.tooldia
  1193. if tooldia == 0:
  1194. for geo in self.gcode_parsed:
  1195. linespec = '--'
  1196. linecolor = color[geo['kind'][0]][1]
  1197. if geo['kind'][0] == 'C':
  1198. linespec = 'k-'
  1199. x, y = geo['geom'].coords.xy
  1200. axes.plot(x, y, linespec, color=linecolor)
  1201. else:
  1202. for geo in self.gcode_parsed:
  1203. poly = geo['geom'].buffer(tooldia/2.0).simplify(tool_tolerance)
  1204. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  1205. edgecolor=color[geo['kind'][0]][1],
  1206. alpha=alpha[geo['kind'][0]], zorder=2)
  1207. axes.add_patch(patch)
  1208. def create_geometry(self):
  1209. # TODO: This takes forever. Too much data?
  1210. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  1211. def polygon2gcode(self, polygon, tolerance=0):
  1212. """
  1213. Creates G-Code for the exterior and all interior paths
  1214. of a polygon.
  1215. :param polygon: A Shapely.Polygon
  1216. :type polygon: Shapely.Polygon
  1217. :param tolerance: All points in the simplified object will be within the
  1218. tolerance distance of the original geometry.
  1219. :type tolerance: float
  1220. :return: G-code to cut along polygon.
  1221. :rtype: str
  1222. """
  1223. if tolerance > 0:
  1224. target_polygon = polygon.simplify(tolerance)
  1225. else:
  1226. target_polygon = polygon
  1227. gcode = ""
  1228. t = "G0%d X%.4fY%.4f\n"
  1229. path = list(target_polygon.exterior.coords) # Polygon exterior
  1230. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1231. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1232. for pt in path[1:]:
  1233. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1234. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1235. for ints in target_polygon.interiors: # Polygon interiors
  1236. path = list(ints.coords)
  1237. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1238. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1239. for pt in path[1:]:
  1240. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1241. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1242. return gcode
  1243. def linear2gcode(self, linear, tolerance=0):
  1244. """
  1245. Generates G-code to cut along the linear feature.
  1246. :param linear: The path to cut along.
  1247. :type: Shapely.LinearRing or Shapely.Linear String
  1248. :param tolerance: All points in the simplified object will be within the
  1249. tolerance distance of the original geometry.
  1250. :type tolerance: float
  1251. :return: G-code to cut alon the linear feature.
  1252. :rtype: str
  1253. """
  1254. if tolerance > 0:
  1255. target_linear = linear.simplify(tolerance)
  1256. else:
  1257. target_linear = linear
  1258. gcode = ""
  1259. t = "G0%d X%.4fY%.4f\n"
  1260. path = list(target_linear.coords)
  1261. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1262. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1263. for pt in path[1:]:
  1264. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1265. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1266. return gcode
  1267. def point2gcode(self, point):
  1268. # TODO: This is not doing anything.
  1269. gcode = ""
  1270. t = "G0%d X%.4fY%.4f\n"
  1271. path = list(point.coords)
  1272. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1273. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1274. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1275. def scale(self, factor):
  1276. """
  1277. Scales all the geometry on the XY plane in the object by the
  1278. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  1279. not altered.
  1280. :param factor: Number by which to scale the object.
  1281. :type factor: float
  1282. :return: None
  1283. :rtype: None
  1284. """
  1285. for g in self.gcode_parsed:
  1286. g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1287. self.create_geometry()
  1288. def offset(self, vect):
  1289. """
  1290. Offsets all the geometry on the XY plane in the object by the
  1291. given vector.
  1292. :param vect: (x, y) offset vector.
  1293. :type vect: tuple
  1294. :return: None
  1295. """
  1296. dx, dy = vect
  1297. for g in self.gcode_parsed:
  1298. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  1299. self.create_geometry()
  1300. def get_bounds(geometry_set):
  1301. xmin = Inf
  1302. ymin = Inf
  1303. xmax = -Inf
  1304. ymax = -Inf
  1305. print "Getting bounds of:", str(geometry_set)
  1306. for gs in geometry_set:
  1307. try:
  1308. gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
  1309. xmin = min([xmin, gxmin])
  1310. ymin = min([ymin, gymin])
  1311. xmax = max([xmax, gxmax])
  1312. ymax = max([ymax, gymax])
  1313. except:
  1314. print "DEV WARNING: Tried to get bounds of empty geometry."
  1315. return [xmin, ymin, xmax, ymax]
  1316. def arc(center, radius, start, stop, direction, steps_per_circ):
  1317. """
  1318. Creates a list of point along the specified arc.
  1319. :param center: Coordinates of the center [x, y]
  1320. :type center: list
  1321. :param radius: Radius of the arc.
  1322. :type radius: float
  1323. :param start: Starting angle in radians
  1324. :type start: float
  1325. :param stop: End angle in radians
  1326. :type stop: float
  1327. :param direction: Orientation of the arc, "CW" or "CCW"
  1328. :type direction: string
  1329. :param steps_per_circ: Number of straight line segments to
  1330. represent a circle.
  1331. :type steps_per_circ: int
  1332. :return: The desired arc, as list of tuples
  1333. :rtype: list
  1334. """
  1335. # TODO: Resolution should be established by fraction of total length, not angle.
  1336. da_sign = {"cw": -1.0, "ccw": 1.0}
  1337. points = []
  1338. if direction == "ccw" and stop <= start:
  1339. stop += 2*pi
  1340. if direction == "cw" and stop >= start:
  1341. stop -= 2*pi
  1342. angle = abs(stop - start)
  1343. #angle = stop-start
  1344. steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2])
  1345. delta_angle = da_sign[direction]*angle*1.0/steps
  1346. for i in range(steps+1):
  1347. theta = start + delta_angle*i
  1348. points.append((center[0]+radius*cos(theta), center[1]+radius*sin(theta)))
  1349. return points
  1350. def clear_poly(poly, tooldia, overlap=0.1):
  1351. """
  1352. Creates a list of Shapely geometry objects covering the inside
  1353. of a Shapely.Polygon. Use for removing all the copper in a region
  1354. or bed flattening.
  1355. :param poly: Target polygon
  1356. :type poly: Shapely.Polygon
  1357. :param tooldia: Diameter of the tool
  1358. :type tooldia: float
  1359. :param overlap: Fraction of the tool diameter to overlap
  1360. in each pass.
  1361. :type overlap: float
  1362. :return: list of Shapely.Polygon
  1363. :rtype: list
  1364. """
  1365. poly_cuts = [poly.buffer(-tooldia/2.0)]
  1366. while True:
  1367. poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  1368. if poly.area > 0:
  1369. poly_cuts.append(poly)
  1370. else:
  1371. break
  1372. return poly_cuts
  1373. def find_polygon(poly_set, point):
  1374. """
  1375. Return the first polygon in the list of polygons poly_set
  1376. that contains the given point.
  1377. """
  1378. p = Point(point)
  1379. for poly in poly_set:
  1380. if poly.contains(p):
  1381. return poly
  1382. return None
  1383. def to_dict(geo):
  1384. output = ''
  1385. if isinstance(geo, BaseGeometry):
  1386. return {
  1387. "__class__": "Shply",
  1388. "__inst__": sdumps(geo)
  1389. }
  1390. return geo
  1391. def dict2obj(d):
  1392. if '__class__' in d and '__inst__' in d:
  1393. # For now assume all classes are Shapely geometry.
  1394. return sloads(d['__inst__'])
  1395. else:
  1396. return d
  1397. def plotg(geo):
  1398. try:
  1399. _ = iter(geo)
  1400. except:
  1401. geo = [geo]
  1402. for g in geo:
  1403. if type(g) == Polygon:
  1404. x, y = g.exterior.coords.xy
  1405. plot(x, y)
  1406. for ints in g.interiors:
  1407. x, y = ints.coords.xy
  1408. plot(x, y)
  1409. continue
  1410. if type(g) == LineString or type(g) == LinearRing:
  1411. x, y = g.coords.xy
  1412. plot(x, y)
  1413. continue
  1414. if type(g) == Point:
  1415. x, y = g.coords.xy
  1416. plot(x, y, 'o')
  1417. continue
  1418. try:
  1419. _ = iter(g)
  1420. plotg(g)
  1421. except:
  1422. print "Cannot plot:", str(type(g))
  1423. continue
  1424. def parse_gerber_number(strnumber, frac_digits):
  1425. """
  1426. Parse a single number of Gerber coordinates.
  1427. :param strnumber: String containing a number in decimal digits
  1428. from a coordinate data block, possibly with a leading sign.
  1429. :type strnumber: str
  1430. :param frac_digits: Number of digits used for the fractional
  1431. part of the number
  1432. :type frac_digits: int
  1433. :return: The number in floating point.
  1434. :rtype: float
  1435. """
  1436. return int(strnumber)*(10**(-frac_digits))
  1437. def parse_gerber_coords(gstr, int_digits, frac_digits):
  1438. """
  1439. Parse Gerber coordinates
  1440. :param gstr: Line of G-Code containing coordinates.
  1441. :type gstr: str
  1442. :param int_digits: Number of digits in integer part of a number.
  1443. :type int_digits: int
  1444. :param frac_digits: Number of digits in frac_digits part of a number.
  1445. :type frac_digits: int
  1446. :return: [x, y] coordinates.
  1447. :rtype: list
  1448. """
  1449. global gerbx, gerby
  1450. xindex = gstr.find("X")
  1451. yindex = gstr.find("Y")
  1452. index = gstr.find("D")
  1453. if xindex == -1:
  1454. x = gerbx
  1455. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  1456. elif yindex == -1:
  1457. y = gerby
  1458. x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
  1459. else:
  1460. x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
  1461. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  1462. gerbx = x
  1463. gerby = y
  1464. return [x, y]