camlib.py 83 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475
  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(object):
  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. This method will look only for only and all the
  145. attributes in ``self.ser_attrs``. They must all
  146. be present. Use only for deserializing saved
  147. objects.
  148. :param d: Dictionary of attributes to set in the object.
  149. :type d: dict
  150. :return: None
  151. """
  152. for attr in self.ser_attrs:
  153. setattr(self, attr, d[attr])
  154. class ApertureMacro:
  155. ## Regular expressions
  156. am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
  157. am2_re = re.compile(r'(.*)%$')
  158. amcomm_re = re.compile(r'^0(.*)')
  159. amprim_re = re.compile(r'^[1-9].*')
  160. amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
  161. def __init__(self, name=None):
  162. self.name = name
  163. self.raw = ""
  164. ## These below are recomputed for every aperture
  165. ## definition, in other words, are temporary variables.
  166. self.primitives = []
  167. self.locvars = {}
  168. self.geometry = None
  169. def to_dict(self):
  170. """
  171. Returns the object in a serializable form. Only the name and
  172. raw are required.
  173. :return: Dictionary representing the object. JSON ready.
  174. :rtype: dict
  175. """
  176. return {
  177. 'name': self.name,
  178. 'raw': self.raw
  179. }
  180. def from_dict(self, d):
  181. """
  182. Populates the object from a serial representation created
  183. with ``self.to_dict()``.
  184. :param d: Serial representation of an ApertureMacro object.
  185. :return: None
  186. """
  187. for attr in ['name', 'raw']:
  188. setattr(self, attr, d[attr])
  189. def parse_content(self):
  190. """
  191. Creates numerical lists for all primitives in the aperture
  192. macro (in ``self.raw``) by replacing all variables by their
  193. values iteratively and evaluating expressions. Results
  194. are stored in ``self.primitives``.
  195. :return: None
  196. """
  197. # Cleanup
  198. self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
  199. self.primitives = []
  200. # Separate parts
  201. parts = self.raw.split('*')
  202. #### Every part in the macro ####
  203. for part in parts:
  204. ### Comments. Ignored.
  205. match = ApertureMacro.amcomm_re.search(part)
  206. if match:
  207. continue
  208. ### Variables
  209. # These are variables defined locally inside the macro. They can be
  210. # numerical constant or defind in terms of previously define
  211. # variables, which can be defined locally or in an aperture
  212. # definition. All replacements ocurr here.
  213. match = ApertureMacro.amvar_re.search(part)
  214. if match:
  215. var = match.group(1)
  216. val = match.group(2)
  217. # Replace variables in value
  218. for v in self.locvars:
  219. val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val)
  220. # Make all others 0
  221. val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
  222. # Change x with *
  223. val = re.sub(r'[xX]', "*", val)
  224. # Eval() and store.
  225. self.locvars[var] = eval(val)
  226. continue
  227. ### Primitives
  228. # Each is an array. The first identifies the primitive, while the
  229. # rest depend on the primitive. All are strings representing a
  230. # number and may contain variable definition. The values of these
  231. # variables are defined in an aperture definition.
  232. match = ApertureMacro.amprim_re.search(part)
  233. if match:
  234. ## Replace all variables
  235. for v in self.locvars:
  236. part = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
  237. # Make all others 0
  238. part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
  239. # Change x with *
  240. part = re.sub(r'[xX]', "*", part)
  241. ## Store
  242. elements = part.split(",")
  243. self.primitives.append([eval(x) for x in elements])
  244. continue
  245. print "WARNING: Unknown syntax of aperture macro part:", part
  246. def append(self, data):
  247. """
  248. Appends a string to the raw macro.
  249. :param data: Part of the macro.
  250. :type data: str
  251. :return: None
  252. """
  253. self.raw += data
  254. @staticmethod
  255. def default2zero(n, mods):
  256. """
  257. Pads the ``mods`` list with zeros resulting in an
  258. list of length n.
  259. :param n: Length of the resulting list.
  260. :type n: int
  261. :param mods: List to be padded.
  262. :type mods: list
  263. :return: Zero-padded list.
  264. :rtype: list
  265. """
  266. x = [0.0]*n
  267. na = len(mods)
  268. x[0:na] = mods
  269. return x
  270. @staticmethod
  271. def make_circle(mods):
  272. """
  273. :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
  274. :return:
  275. """
  276. pol, dia, x, y = ApertureMacro.default2zero(4, mods)
  277. return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
  278. @staticmethod
  279. def make_vectorline(mods):
  280. """
  281. :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
  282. rotation angle around origin in degrees)
  283. :return:
  284. """
  285. pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
  286. line = LineString([(xs, ys), (xe, ye)])
  287. box = line.buffer(width/2, cap_style=2)
  288. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  289. return {"pol": int(pol), "geometry": box_rotated}
  290. @staticmethod
  291. def make_centerline(mods):
  292. """
  293. :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
  294. rotation angle around origin in degrees)
  295. :return:
  296. """
  297. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  298. box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
  299. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  300. return {"pol": int(pol), "geometry": box_rotated}
  301. @staticmethod
  302. def make_lowerleftline(mods):
  303. """
  304. :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
  305. rotation angle around origin in degrees)
  306. :return:
  307. """
  308. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  309. box = shply_box(x, y, x+width, y+height)
  310. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  311. return {"pol": int(pol), "geometry": box_rotated}
  312. @staticmethod
  313. def make_outline(mods):
  314. """
  315. :param mods:
  316. :return:
  317. """
  318. pol = mods[0]
  319. n = mods[1]
  320. points = [(0, 0)]*(n+1)
  321. for i in range(n+1):
  322. points[i] = mods[2*i + 2:2*i + 4]
  323. angle = mods[2*n + 4]
  324. poly = Polygon(points)
  325. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  326. return {"pol": int(pol), "geometry": poly_rotated}
  327. @staticmethod
  328. def make_polygon(mods):
  329. """
  330. Note: Specs indicate that rotation is only allowed if the center
  331. (x, y) == (0, 0). I will tolerate breaking this rule.
  332. :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
  333. diameter of circumscribed circle >=0, rotation angle around origin)
  334. :return:
  335. """
  336. pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
  337. points = [(0, 0)]*nverts
  338. for i in range(nverts):
  339. points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
  340. y + 0.5 * dia * sin(2*pi * i/nverts))
  341. poly = Polygon(points)
  342. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  343. return {"pol": int(pol), "geometry": poly_rotated}
  344. @staticmethod
  345. def make_moire(mods):
  346. """
  347. Note: Specs indicate that rotation is only allowed if the center
  348. (x, y) == (0, 0). I will tolerate breaking this rule.
  349. :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
  350. gap, max_rings, crosshair_thickness, crosshair_len, rotation
  351. angle around origin in degrees)
  352. :return:
  353. """
  354. x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
  355. r = dia/2 - thickness/2
  356. result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  357. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) # Need a copy!
  358. i = 1 # Number of rings created so far
  359. ## If the ring does not have an interior it means that it is
  360. ## a disk. Then stop.
  361. while len(ring.interiors) > 0 and i < nrings:
  362. r -= thickness + gap
  363. if r <= 0:
  364. break
  365. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  366. result = cascaded_union([result, ring])
  367. i += 1
  368. ## Crosshair
  369. hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
  370. ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
  371. result = cascaded_union([result, hor, ver])
  372. return {"pol": 1, "geometry": result}
  373. @staticmethod
  374. def make_thermal(mods):
  375. """
  376. Note: Specs indicate that rotation is only allowed if the center
  377. (x, y) == (0, 0). I will tolerate breaking this rule.
  378. :param mods: [x-center, y-center, diameter-outside, diameter-inside,
  379. gap-thickness, rotation angle around origin]
  380. :return:
  381. """
  382. x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
  383. ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
  384. hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
  385. vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
  386. thermal = ring.difference(hline.union(vline))
  387. return {"pol": 1, "geometry": thermal}
  388. def make_geometry(self, modifiers):
  389. """
  390. Runs the macro for the given modifiers and generates
  391. the corresponding geometry.
  392. :param modifiers: Modifiers (parameters) for this macro
  393. :type modifiers: list
  394. """
  395. ## Primitive makers
  396. makers = {
  397. "1": ApertureMacro.make_circle,
  398. "2": ApertureMacro.make_vectorline,
  399. "20": ApertureMacro.make_vectorline,
  400. "21": ApertureMacro.make_centerline,
  401. "22": ApertureMacro.make_lowerleftline,
  402. "4": ApertureMacro.make_outline,
  403. "5": ApertureMacro.make_polygon,
  404. "6": ApertureMacro.make_moire,
  405. "7": ApertureMacro.make_thermal
  406. }
  407. ## Store modifiers as local variables
  408. modifiers = modifiers or []
  409. modifiers = [float(m) for m in modifiers]
  410. self.locvars = {}
  411. for i in range(0, len(modifiers)):
  412. self.locvars[str(i+1)] = modifiers[i]
  413. ## Parse
  414. self.primitives = [] # Cleanup
  415. self.geometry = None
  416. self.parse_content()
  417. ## Make the geometry
  418. for primitive in self.primitives:
  419. # Make the primitive
  420. prim_geo = makers[str(int(primitive[0]))](primitive[1:])
  421. # Add it (according to polarity)
  422. if self.geometry is None and prim_geo['pol'] == 1:
  423. self.geometry = prim_geo['geometry']
  424. continue
  425. if prim_geo['pol'] == 1:
  426. self.geometry = self.geometry.union(prim_geo['geometry'])
  427. continue
  428. if prim_geo['pol'] == 0:
  429. self.geometry = self.geometry.difference(prim_geo['geometry'])
  430. continue
  431. return self.geometry
  432. class Gerber (Geometry):
  433. """
  434. **ATTRIBUTES**
  435. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  436. The values are dictionaries key/value pairs which describe the aperture. The
  437. type key is always present and the rest depend on the key:
  438. +-----------+-----------------------------------+
  439. | Key | Value |
  440. +===========+===================================+
  441. | type | (str) "C", "R", "O", "P", or "AP" |
  442. +-----------+-----------------------------------+
  443. | others | Depend on ``type`` |
  444. +-----------+-----------------------------------+
  445. * ``paths`` (list): A path is described by a line an aperture that follows that
  446. line. Each paths[i] is a dictionary:
  447. +------------+------------------------------------------------+
  448. | Key | Value |
  449. +============+================================================+
  450. | linestring | (Shapely.LineString) The actual path. |
  451. +------------+------------------------------------------------+
  452. | aperture | (str) The key for an aperture in apertures. |
  453. +------------+------------------------------------------------+
  454. * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
  455. is a dictionary:
  456. +------------+------------------------------------------------+
  457. | Key | Value |
  458. +============+================================================+
  459. | loc | (Point) Shapely Point indicating location. |
  460. +------------+------------------------------------------------+
  461. | aperture | (str) The key for an aperture in apertures. |
  462. +------------+------------------------------------------------+
  463. * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
  464. which have an exterior and zero or more interiors. An aperture is also
  465. associated with a region. Each is a dictionary:
  466. +------------+-----------------------------------------------------+
  467. | Key | Value |
  468. +============+=====================================================+
  469. | polygon | (Shapely.Polygon) The polygon defining the region. |
  470. +------------+-----------------------------------------------------+
  471. | aperture | (str) The key for an aperture in apertures. |
  472. +------------+-----------------------------------------------------+
  473. * ``aperture_macros`` (dictionary): Are predefined geometrical structures
  474. that can be instanciated with different parameters in an aperture
  475. definition. See ``apertures`` above. The key is the name of the macro,
  476. and the macro itself, the value, is a ``Aperture_Macro`` object.
  477. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  478. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  479. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  480. *buffering* (or thickening) the ``paths`` with the aperture. These are
  481. generated from ``paths`` in ``buffer_paths()``.
  482. **USAGE**::
  483. g = Gerber()
  484. g.parse_file(filename)
  485. g.create_geometry()
  486. do_something(s.solid_geometry)
  487. """
  488. def __init__(self):
  489. """
  490. The constructor takes no parameters. Use ``gerber.parse_files()``
  491. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  492. :return: Gerber object
  493. :rtype: Gerber
  494. """
  495. # Initialize parent
  496. Geometry.__init__(self)
  497. self.solid_geometry = Polygon()
  498. # Number format
  499. self.int_digits = 3
  500. """Number of integer digits in Gerber numbers. Used during parsing."""
  501. self.frac_digits = 4
  502. """Number of fraction digits in Gerber numbers. Used during parsing."""
  503. ## Gerber elements ##
  504. # Apertures {'id':{'type':chr,
  505. # ['size':float], ['width':float],
  506. # ['height':float]}, ...}
  507. self.apertures = {}
  508. # Paths [{'linestring':LineString, 'aperture':str}]
  509. # self.paths = []
  510. # Buffered Paths [Polygon]
  511. # Paths transformed into Polygons by
  512. # offsetting the aperture size/2
  513. # self.buffered_paths = []
  514. # Polygon regions [{'polygon':Polygon, 'aperture':str}]
  515. # self.regions = []
  516. # Flashes [{'loc':[float,float], 'aperture':str}]
  517. # self.flashes = []
  518. # Geometry from flashes
  519. # self.flash_geometry = []
  520. # Aperture Macros
  521. self.aperture_macros = {}
  522. # Attributes to be included in serialization
  523. # Always append to it because it carries contents
  524. # from Geometry.
  525. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
  526. 'buffered_paths', 'regions', 'flashes',
  527. 'flash_geometry', 'aperture_macros']
  528. #### Parser patterns ####
  529. # FS - Format Specification
  530. # The format of X and Y must be the same!
  531. # L-omit leading zeros, T-omit trailing zeros
  532. # A-absolute notation, I-incremental notation
  533. self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
  534. # Mode (IN/MM)
  535. self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
  536. # Comment G04|G4
  537. self.comm_re = re.compile(r'^G0?4(.*)$')
  538. # AD - Aperture definition
  539. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*)(?:,(.*))?\*%$')
  540. # AM - Aperture Macro
  541. # Beginning of macro (Ends with *%):
  542. self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  543. # Tool change
  544. # May begin with G54 but that is deprecated
  545. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  546. # G01... - Linear interpolation plus flashes with coordinates
  547. # Operation code (D0x) missing is deprecated... oh well I will support it.
  548. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X(-?\d+))?(?=.*Y(-?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  549. # Operation code alone, usually just D03 (Flash)
  550. self.opcode_re = re.compile(r'^D0?([123])\*$')
  551. # G02/3... - Circular interpolation with coordinates
  552. # 2-clockwise, 3-counterclockwise
  553. # Operation code (D0x) missing is deprecated... oh well I will support it.
  554. # Optional start with G02 or G03, optional end with D01 or D02 with
  555. # optional coordinates but at least one in any order.
  556. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X(-?\d+))?(?=.*Y(-?\d+))' +
  557. '?(?=.*I(-?\d+))?(?=.*J(-?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  558. # G01/2/3 Occurring without coordinates
  559. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  560. # Single D74 or multi D75 quadrant for circular interpolation
  561. self.quad_re = re.compile(r'^G7([45])\*$')
  562. # Region mode on
  563. # In region mode, D01 starts a region
  564. # and D02 ends it. A new region can be started again
  565. # with D01. All contours must be closed before
  566. # D02 or G37.
  567. self.regionon_re = re.compile(r'^G36\*$')
  568. # Region mode off
  569. # Will end a region and come off region mode.
  570. # All contours must be closed before D02 or G37.
  571. self.regionoff_re = re.compile(r'^G37\*$')
  572. # End of file
  573. self.eof_re = re.compile(r'^M02\*')
  574. # IP - Image polarity
  575. self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
  576. # LP - Level polarity
  577. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  578. # Units (OBSOLETE)
  579. self.units_re = re.compile(r'^G7([01])\*$')
  580. # Absolute/Relative G90/1 (OBSOLETE)
  581. self.absrel_re = re.compile(r'^G9([01])\*$')
  582. # Aperture macros
  583. self.am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
  584. self.am2_re = re.compile(r'(.*)%$')
  585. # TODO: This is bad.
  586. self.steps_per_circ = 40
  587. def scale(self, factor):
  588. """
  589. Scales the objects' geometry on the XY plane by a given factor.
  590. These are:
  591. * ``buffered_paths``
  592. * ``flash_geometry``
  593. * ``solid_geometry``
  594. * ``regions``
  595. NOTE:
  596. Does not modify the data used to create these elements. If these
  597. are recreated, the scaling will be lost. This behavior was modified
  598. because of the complexity reached in this class.
  599. :param factor: Number by which to scale.
  600. :type factor: float
  601. :rtype : None
  602. """
  603. ## solid_geometry ???
  604. # It's a cascaded union of objects.
  605. self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  606. factor, origin=(0, 0))
  607. # # Now buffered_paths, flash_geometry and solid_geometry
  608. # self.create_geometry()
  609. def offset(self, vect):
  610. """
  611. Offsets the objects' geometry on the XY plane by a given vector.
  612. These are:
  613. * ``buffered_paths``
  614. * ``flash_geometry``
  615. * ``solid_geometry``
  616. * ``regions``
  617. NOTE:
  618. Does not modify the data used to create these elements. If these
  619. are recreated, the scaling will be lost. This behavior was modified
  620. because of the complexity reached in this class.
  621. :param vect: (x, y) offset vector.
  622. :type vect: tuple
  623. :return: None
  624. """
  625. dx, dy = vect
  626. ## Solid geometry
  627. self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
  628. def mirror(self, axis, point):
  629. """
  630. Mirrors the object around a specified axis passign through
  631. the given point. What is affected:
  632. * ``buffered_paths``
  633. * ``flash_geometry``
  634. * ``solid_geometry``
  635. * ``regions``
  636. NOTE:
  637. Does not modify the data used to create these elements. If these
  638. are recreated, the scaling will be lost. This behavior was modified
  639. because of the complexity reached in this class.
  640. :param axis: "X" or "Y" indicates around which axis to mirror.
  641. :type axis: str
  642. :param point: [x, y] point belonging to the mirror axis.
  643. :type point: list
  644. :return: None
  645. """
  646. px, py = point
  647. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  648. ## solid_geometry ???
  649. # It's a cascaded union of objects.
  650. self.solid_geometry = affinity.scale(self.solid_geometry,
  651. xscale, yscale, origin=(px, py))
  652. def aperture_parse(self, apertureId, apertureType, apParameters):
  653. """
  654. Parse gerber aperture definition into dictionary of apertures.
  655. The following kinds and their attributes are supported:
  656. * *Circular (C)*: size (float)
  657. * *Rectangle (R)*: width (float), height (float)
  658. * *Obround (O)*: width (float), height (float).
  659. * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
  660. * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
  661. :param apertureId: Id of the aperture being defined.
  662. :param apertureType: Type of the aperture.
  663. :param apParameters: Parameters of the aperture.
  664. :type apertureId: str
  665. :type apertureType: str
  666. :type apParameters: str
  667. :return: Identifier of the aperture.
  668. :rtype: str
  669. """
  670. # Found some Gerber with a leading zero in the aperture id and the
  671. # referenced it without the zero, so this is a hack to handle that.
  672. apid = str(int(apertureId))
  673. try: # Could be empty for aperture macros
  674. paramList = apParameters.split('X')
  675. except:
  676. paramList = None
  677. if apertureType == "C": # Circle, example: %ADD11C,0.1*%
  678. self.apertures[apid] = {"type": "C",
  679. "size": float(paramList[0])}
  680. return apid
  681. if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*%
  682. self.apertures[apid] = {"type": "R",
  683. "width": float(paramList[0]),
  684. "height": float(paramList[1])}
  685. return apid
  686. if apertureType == "O": # Obround
  687. self.apertures[apid] = {"type": "O",
  688. "width": float(paramList[0]),
  689. "height": float(paramList[1])}
  690. return apid
  691. if apertureType == "P": # Polygon (regular)
  692. self.apertures[apid] = {"type": "P",
  693. "diam": float(paramList[0]),
  694. "nVertices": int(paramList[1])}
  695. if len(paramList) >= 3:
  696. self.apertures[apid]["rotation"] = float(paramList[2])
  697. return apid
  698. if apertureType in self.aperture_macros:
  699. self.apertures[apid] = {"type": "AM",
  700. "macro": self.aperture_macros[apertureType],
  701. "modifiers": paramList}
  702. return apid
  703. print "WARNING: Aperture not implemented:", apertureType
  704. return None
  705. def parse_file(self, filename):
  706. """
  707. Calls Gerber.parse_lines() with array of lines
  708. read from the given file.
  709. :param filename: Gerber file to parse.
  710. :type filename: str
  711. :return: None
  712. """
  713. gfile = open(filename, 'r')
  714. gstr = gfile.readlines()
  715. gfile.close()
  716. self.parse_lines(gstr)
  717. def parse_lines(self, glines):
  718. """
  719. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  720. ``self.flashes``, ``self.regions`` and ``self.units``.
  721. :param glines: Gerber code as list of strings, each element being
  722. one line of the source file.
  723. :type glines: list
  724. :return: None
  725. :rtype: None
  726. """
  727. # Coordinates of the current path, each is [x, y]
  728. path = []
  729. # Polygons are stored here until there is a change in polarity.
  730. # Only then they are combined via cascaded_union and added or
  731. # subtracted from solid_geometry. This is ~100 times faster than
  732. # applyng a union for every new polygon.
  733. poly_buffer = []
  734. last_path_aperture = None
  735. current_aperture = None
  736. # 1,2 or 3 from "G01", "G02" or "G03"
  737. current_interpolation_mode = None
  738. # 1 or 2 from "D01" or "D02"
  739. # Note this is to support deprecated Gerber not putting
  740. # an operation code at the end of every coordinate line.
  741. current_operation_code = None
  742. # Current coordinates
  743. current_x = None
  744. current_y = None
  745. # Absolute or Relative/Incremental coordinates
  746. # Not implemented
  747. absolute = True
  748. # How to interpret circular interpolation: SINGLE or MULTI
  749. quadrant_mode = None
  750. # Indicates we are parsing an aperture macro
  751. current_macro = None
  752. # Indicates the current polarity: D-Dark, C-Clear
  753. current_polarity = 'D'
  754. # If a region is being defined
  755. making_region = False
  756. #### Parsing starts here ####
  757. line_num = 0
  758. for gline in glines:
  759. line_num += 1
  760. ### Cleanup
  761. gline = gline.strip(' \r\n')
  762. ### Aperture Macros
  763. # Having this at the beggining will slow things down
  764. # but macros can have complicated statements than could
  765. # be caught by other ptterns.
  766. if current_macro is None: # No macro started yet
  767. match = self.am1_re.search(gline)
  768. # Start macro if match, else not an AM, carry on.
  769. if match:
  770. current_macro = match.group(1)
  771. self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
  772. if match.group(2): # Append
  773. self.aperture_macros[current_macro].append(match.group(2))
  774. if match.group(3): # Finish macro
  775. #self.aperture_macros[current_macro].parse_content()
  776. current_macro = None
  777. continue
  778. else: # Continue macro
  779. match = self.am2_re.search(gline)
  780. if match: # Finish macro
  781. self.aperture_macros[current_macro].append(match.group(1))
  782. #self.aperture_macros[current_macro].parse_content()
  783. current_macro = None
  784. else: # Append
  785. self.aperture_macros[current_macro].append(gline)
  786. continue
  787. ### G01 - Linear interpolation plus flashes
  788. # Operation code (D0x) missing is deprecated... oh well I will support it.
  789. # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
  790. match = self.lin_re.search(gline)
  791. if match:
  792. # Dxx alone?
  793. # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
  794. # try:
  795. # current_operation_code = int(match.group(4))
  796. # except:
  797. # pass # A line with just * will match too.
  798. # continue
  799. # NOTE: Letting it continue allows it to react to the
  800. # operation code.
  801. # Parse coordinates
  802. if match.group(2) is not None:
  803. current_x = parse_gerber_number(match.group(2), self.frac_digits)
  804. if match.group(3) is not None:
  805. current_y = parse_gerber_number(match.group(3), self.frac_digits)
  806. # Parse operation code
  807. if match.group(4) is not None:
  808. current_operation_code = int(match.group(4))
  809. # Pen down: add segment
  810. if current_operation_code == 1:
  811. path.append([current_x, current_y])
  812. last_path_aperture = current_aperture
  813. elif current_operation_code == 2:
  814. if len(path) > 1:
  815. ## --- BUFFERED ---
  816. if making_region:
  817. geo = Polygon(path)
  818. else:
  819. if last_path_aperture is None:
  820. print "Warning: No aperture defined for curent path. (%d)" % line_num
  821. width = self.apertures[last_path_aperture]["size"]
  822. geo = LineString(path).buffer(width/2)
  823. poly_buffer.append(geo)
  824. path = [[current_x, current_y]] # Start new path
  825. # Flash
  826. elif current_operation_code == 3:
  827. # --- BUFFERED ---
  828. flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
  829. self.apertures[current_aperture])
  830. poly_buffer.append(flash)
  831. continue
  832. ### G02/3 - Circular interpolation
  833. # 2-clockwise, 3-counterclockwise
  834. match = self.circ_re.search(gline)
  835. if match:
  836. mode, x, y, i, j, d = match.groups()
  837. try:
  838. x = parse_gerber_number(x, self.frac_digits)
  839. except:
  840. x = current_x
  841. try:
  842. y = parse_gerber_number(y, self.frac_digits)
  843. except:
  844. y = current_y
  845. try:
  846. i = parse_gerber_number(i, self.frac_digits)
  847. except:
  848. i = 0
  849. try:
  850. j = parse_gerber_number(j, self.frac_digits)
  851. except:
  852. j = 0
  853. if quadrant_mode is None:
  854. print "ERROR: Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num
  855. print gline
  856. continue
  857. if mode is None and current_interpolation_mode not in [2, 3]:
  858. print "ERROR: Found arc without circular interpolation mode defined. (%d)" % line_num
  859. print gline
  860. continue
  861. elif mode is not None:
  862. current_interpolation_mode = int(mode)
  863. # Set operation code if provided
  864. if d is not None:
  865. current_operation_code = int(d)
  866. # Nothing created! Pen Up.
  867. if current_operation_code == 2:
  868. print "Warning: Arc with D2. (%d)" % line_num
  869. if len(path) > 1:
  870. if last_path_aperture is None:
  871. print "Warning: No aperture defined for curent path. (%d)" % line_num
  872. # --- BUFFERED ---
  873. width = self.apertures[last_path_aperture]["size"]
  874. buffered = LineString(path).buffer(width/2)
  875. poly_buffer.append(buffered)
  876. current_x = x
  877. current_y = y
  878. path = [[current_x, current_y]] # Start new path
  879. continue
  880. # Flash should not happen here
  881. if current_operation_code == 3:
  882. print "ERROR: Trying to flash within arc. (%d)" % line_num
  883. continue
  884. if quadrant_mode == 'MULTI':
  885. center = [i + current_x, j + current_y]
  886. radius = sqrt(i**2 + j**2)
  887. start = arctan2(-j, -i)
  888. stop = arctan2(-center[1] + y, -center[0] + x)
  889. arcdir = [None, None, "cw", "ccw"]
  890. this_arc = arc(center, radius, start, stop,
  891. arcdir[current_interpolation_mode],
  892. self.steps_per_circ)
  893. # Last point in path is current point
  894. current_x = this_arc[-1][0]
  895. current_y = this_arc[-1][1]
  896. # Append
  897. path += this_arc
  898. last_path_aperture = current_aperture
  899. continue
  900. if quadrant_mode == 'SINGLE':
  901. print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
  902. ### Operation code alone
  903. match = self.opcode_re.search(gline)
  904. if match:
  905. current_operation_code = int(match.group(1))
  906. if current_operation_code == 3:
  907. ## --- Buffered ---
  908. flash = Gerber.create_flash_geometry(Point(path[-1]),
  909. self.apertures[current_aperture])
  910. poly_buffer.append(flash)
  911. continue
  912. ### G74/75* - Single or multiple quadrant arcs
  913. match = self.quad_re.search(gline)
  914. if match:
  915. if match.group(1) == '4':
  916. quadrant_mode = 'SINGLE'
  917. else:
  918. quadrant_mode = 'MULTI'
  919. continue
  920. ### G36* - Begin region
  921. if self.regionon_re.search(gline):
  922. if len(path) > 1:
  923. # Take care of what is left in the path
  924. ## --- Buffered ---
  925. width = self.apertures[last_path_aperture]["size"]
  926. geo = LineString(path).buffer(width/2)
  927. poly_buffer.append(geo)
  928. path = [path[-1]]
  929. making_region = True
  930. continue
  931. ### G37* - End region
  932. if self.regionoff_re.search(gline):
  933. making_region = False
  934. # Only one path defines region?
  935. # This can happen if D02 happened before G37 and
  936. # is not and error.
  937. if len(path) < 3:
  938. # print "ERROR: Path contains less than 3 points:"
  939. # print path
  940. # print "Line (%d): " % line_num, gline
  941. # path = []
  942. #path = [[current_x, current_y]]
  943. continue
  944. # For regions we may ignore an aperture that is None
  945. # self.regions.append({"polygon": Polygon(path),
  946. # "aperture": last_path_aperture})
  947. # --- Buffered ---
  948. region = Polygon(path)
  949. if not region.is_valid:
  950. region = region.buffer(0)
  951. poly_buffer.append(region)
  952. path = [[current_x, current_y]] # Start new path
  953. continue
  954. ### Aperture definitions %ADD...
  955. match = self.ad_re.search(gline)
  956. if match:
  957. self.aperture_parse(match.group(1), match.group(2), match.group(3))
  958. continue
  959. ### G01/2/3* - Interpolation mode change
  960. # Can occur along with coordinates and operation code but
  961. # sometimes by itself (handled here).
  962. # Example: G01*
  963. match = self.interp_re.search(gline)
  964. if match:
  965. current_interpolation_mode = int(match.group(1))
  966. continue
  967. ### Tool/aperture change
  968. # Example: D12*
  969. match = self.tool_re.search(gline)
  970. if match:
  971. current_aperture = match.group(1)
  972. continue
  973. ### Polarity change
  974. # Example: %LPD*% or %LPC*%
  975. match = self.lpol_re.search(gline)
  976. if match:
  977. if len(path) > 1 and current_polarity != match.group(1):
  978. # --- Buffered ----
  979. width = self.apertures[last_path_aperture]["size"]
  980. geo = LineString(path).buffer(width/2)
  981. poly_buffer.append(geo)
  982. path = [path[-1]]
  983. # --- Apply buffer ---
  984. if current_polarity == 'D':
  985. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  986. else:
  987. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  988. poly_buffer = []
  989. current_polarity = match.group(1)
  990. continue
  991. ### Number format
  992. # Example: %FSLAX24Y24*%
  993. # TODO: This is ignoring most of the format. Implement the rest.
  994. match = self.fmt_re.search(gline)
  995. if match:
  996. absolute = {'A': True, 'I': False}
  997. self.int_digits = int(match.group(3))
  998. self.frac_digits = int(match.group(4))
  999. continue
  1000. ### Mode (IN/MM)
  1001. # Example: %MOIN*%
  1002. match = self.mode_re.search(gline)
  1003. if match:
  1004. self.units = match.group(1)
  1005. continue
  1006. ### Units (G70/1) OBSOLETE
  1007. match = self.units_re.search(gline)
  1008. if match:
  1009. self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
  1010. continue
  1011. ### Absolute/relative coordinates G90/1 OBSOLETE
  1012. match = self.absrel_re.search(gline)
  1013. if match:
  1014. absolute = {'0': True, '1': False}[match.group(1)]
  1015. continue
  1016. #### Ignored lines
  1017. ## Comments
  1018. match = self.comm_re.search(gline)
  1019. if match:
  1020. continue
  1021. ## EOF
  1022. match = self.eof_re.search(gline)
  1023. if match:
  1024. continue
  1025. ### Line did not match any pattern. Warn user.
  1026. print "WARNING: Line ignored (%d):" % line_num, gline
  1027. if len(path) > 1:
  1028. # EOF, create shapely LineString if something still in path
  1029. ## --- Buffered ---
  1030. width = self.apertures[last_path_aperture]["size"]
  1031. geo = LineString(path).buffer(width/2)
  1032. poly_buffer.append(geo)
  1033. # --- Apply buffer ---
  1034. if current_polarity == 'D':
  1035. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  1036. else:
  1037. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  1038. @staticmethod
  1039. def create_flash_geometry(location, aperture):
  1040. if type(location) == list:
  1041. location = Point(location)
  1042. if aperture['type'] == 'C': # Circles
  1043. return location.buffer(aperture['size']/2)
  1044. if aperture['type'] == 'R': # Rectangles
  1045. loc = location.coords[0]
  1046. width = aperture['width']
  1047. height = aperture['height']
  1048. minx = loc[0] - width/2
  1049. maxx = loc[0] + width/2
  1050. miny = loc[1] - height/2
  1051. maxy = loc[1] + height/2
  1052. return shply_box(minx, miny, maxx, maxy)
  1053. if aperture['type'] == 'O': # Obround
  1054. loc = location.coords[0]
  1055. width = aperture['width']
  1056. height = aperture['height']
  1057. if width > height:
  1058. p1 = Point(loc[0] + 0.5*(width-height), loc[1])
  1059. p2 = Point(loc[0] - 0.5*(width-height), loc[1])
  1060. c1 = p1.buffer(height*0.5)
  1061. c2 = p2.buffer(height*0.5)
  1062. else:
  1063. p1 = Point(loc[0], loc[1] + 0.5*(height-width))
  1064. p2 = Point(loc[0], loc[1] - 0.5*(height-width))
  1065. c1 = p1.buffer(width*0.5)
  1066. c2 = p2.buffer(width*0.5)
  1067. return cascaded_union([c1, c2]).convex_hull
  1068. if aperture['type'] == 'P': # Regular polygon
  1069. loc = location.coords[0]
  1070. diam = aperture['diam']
  1071. n_vertices = aperture['nVertices']
  1072. points = []
  1073. for i in range(0, n_vertices):
  1074. x = loc[0] + diam * (cos(2 * pi * i / n_vertices))
  1075. y = loc[1] + diam * (sin(2 * pi * i / n_vertices))
  1076. points.append((x, y))
  1077. ply = Polygon(points)
  1078. if 'rotation' in aperture:
  1079. ply = affinity.rotate(ply, aperture['rotation'])
  1080. return ply
  1081. if aperture['type'] == 'AM': # Aperture Macro
  1082. loc = location.coords[0]
  1083. flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
  1084. return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
  1085. return None
  1086. def create_geometry(self):
  1087. """
  1088. Geometry from a Gerber file is made up entirely of polygons.
  1089. Every stroke (linear or circular) has an aperture which gives
  1090. it thickness. Additionally, aperture strokes have non-zero area,
  1091. and regions naturally do as well.
  1092. :rtype : None
  1093. :return: None
  1094. """
  1095. # self.buffer_paths()
  1096. #
  1097. # self.fix_regions()
  1098. #
  1099. # self.do_flashes()
  1100. #
  1101. # self.solid_geometry = cascaded_union(self.buffered_paths +
  1102. # [poly['polygon'] for poly in self.regions] +
  1103. # self.flash_geometry)
  1104. def get_bounding_box(self, margin=0.0, rounded=False):
  1105. """
  1106. Creates and returns a rectangular polygon bounding at a distance of
  1107. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  1108. can optionally have rounded corners of radius equal to margin.
  1109. :param margin: Distance to enlarge the rectangular bounding
  1110. box in both positive and negative, x and y axes.
  1111. :type margin: float
  1112. :param rounded: Wether or not to have rounded corners.
  1113. :type rounded: bool
  1114. :return: The bounding box.
  1115. :rtype: Shapely.Polygon
  1116. """
  1117. bbox = self.solid_geometry.envelope.buffer(margin)
  1118. if not rounded:
  1119. bbox = bbox.envelope
  1120. return bbox
  1121. class Excellon(Geometry):
  1122. """
  1123. *ATTRIBUTES*
  1124. * ``tools`` (dict): The key is the tool name and the value is
  1125. a dictionary specifying the tool:
  1126. ================ ====================================
  1127. Key Value
  1128. ================ ====================================
  1129. C Diameter of the tool
  1130. Others Not supported (Ignored).
  1131. ================ ====================================
  1132. * ``drills`` (list): Each is a dictionary:
  1133. ================ ====================================
  1134. Key Value
  1135. ================ ====================================
  1136. point (Shapely.Point) Where to drill
  1137. tool (str) A key in ``tools``
  1138. ================ ====================================
  1139. """
  1140. def __init__(self):
  1141. """
  1142. The constructor takes no parameters.
  1143. :return: Excellon object.
  1144. :rtype: Excellon
  1145. """
  1146. Geometry.__init__(self)
  1147. self.tools = {}
  1148. self.drills = []
  1149. # Trailing "T" or leading "L"
  1150. self.zeros = ""
  1151. # Attributes to be included in serialization
  1152. # Always append to it because it carries contents
  1153. # from Geometry.
  1154. self.ser_attrs += ['tools', 'drills', 'zeros']
  1155. #### Patterns ####
  1156. # Regex basics:
  1157. # ^ - beginning
  1158. # $ - end
  1159. # *: 0 or more, +: 1 or more, ?: 0 or 1
  1160. # M48 - Beggining of Part Program Header
  1161. self.hbegin_re = re.compile(r'^M48$')
  1162. # M95 or % - End of Part Program Header
  1163. # NOTE: % has different meaning in the body
  1164. self.hend_re = re.compile(r'^(?:M95|%)$')
  1165. # FMAT Excellon format
  1166. self.fmat_re = re.compile(r'^FMAT,([12])$')
  1167. # Number format and units
  1168. # INCH uses 6 digits
  1169. # METRIC uses 5/6
  1170. self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
  1171. # Tool definition/parameters (?= is look-ahead
  1172. # NOTE: This might be an overkill!
  1173. # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
  1174. # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  1175. # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  1176. # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  1177. self.toolset_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))?' +
  1178. r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  1179. r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  1180. r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  1181. # Tool select
  1182. # Can have additional data after tool number but
  1183. # is ignored if present in the header.
  1184. # Warning: This will match toolset_re too.
  1185. # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
  1186. self.toolsel_re = re.compile(r'^T(\d+)')
  1187. # Comment
  1188. self.comm_re = re.compile(r'^;(.*)$')
  1189. # Absolute/Incremental G90/G91
  1190. self.absinc_re = re.compile(r'^G9([01])$')
  1191. # Modes of operation
  1192. # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
  1193. self.modes_re = re.compile(r'^G0([012345])')
  1194. # Measuring mode
  1195. # 1-metric, 2-inch
  1196. self.meas_re = re.compile(r'^M7([12])$')
  1197. # Coordinates
  1198. #self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
  1199. #self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
  1200. self.coordsperiod_re = re.compile(r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]')
  1201. self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]')
  1202. # R - Repeat hole (# times, X offset, Y offset)
  1203. self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
  1204. # Various stop/pause commands
  1205. self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
  1206. def parse_file(self, filename):
  1207. """
  1208. Reads the specified file as array of lines as
  1209. passes it to ``parse_lines()``.
  1210. :param filename: The file to be read and parsed.
  1211. :type filename: str
  1212. :return: None
  1213. """
  1214. efile = open(filename, 'r')
  1215. estr = efile.readlines()
  1216. efile.close()
  1217. self.parse_lines(estr)
  1218. def parse_lines(self, elines):
  1219. """
  1220. Main Excellon parser.
  1221. :param elines: List of strings, each being a line of Excellon code.
  1222. :type elines: list
  1223. :return: None
  1224. """
  1225. # State variables
  1226. current_tool = ""
  1227. in_header = False
  1228. current_x = None
  1229. current_y = None
  1230. line_num = 0 # Line number
  1231. for eline in elines:
  1232. line_num += 1
  1233. ### Cleanup
  1234. eline = eline.strip(' \r\n')
  1235. ## Header Begin/End ##
  1236. if self.hbegin_re.search(eline):
  1237. in_header = True
  1238. continue
  1239. if self.hend_re.search(eline):
  1240. in_header = False
  1241. continue
  1242. #### Body ####
  1243. if not in_header:
  1244. ## Tool change ##
  1245. match = self.toolsel_re.search(eline)
  1246. if match:
  1247. current_tool = str(int(match.group(1)))
  1248. continue
  1249. ## Coordinates without period ##
  1250. match = self.coordsnoperiod_re.search(eline)
  1251. if match:
  1252. try:
  1253. x = float(match.group(1))/10000
  1254. current_x = x
  1255. except TypeError:
  1256. x = current_x
  1257. try:
  1258. y = float(match.group(2))/10000
  1259. current_y = y
  1260. except TypeError:
  1261. y = current_y
  1262. if x is None or y is None:
  1263. print "ERROR: Missing coordinates"
  1264. continue
  1265. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  1266. continue
  1267. ## Coordinates with period ##
  1268. match = self.coordsperiod_re.search(eline)
  1269. if match:
  1270. try:
  1271. x = float(match.group(1))
  1272. current_x = x
  1273. except TypeError:
  1274. x = current_x
  1275. try:
  1276. y = float(match.group(2))
  1277. current_y = y
  1278. except TypeError:
  1279. y = current_y
  1280. if x is None or y is None:
  1281. print "ERROR: Missing coordinates"
  1282. continue
  1283. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  1284. continue
  1285. #### Header ####
  1286. if in_header:
  1287. ## Tool definitions ##
  1288. match = self.toolset_re.search(eline)
  1289. if match:
  1290. name = str(int(match.group(1)))
  1291. spec = {
  1292. "C": float(match.group(2)),
  1293. # "F": float(match.group(3)),
  1294. # "S": float(match.group(4)),
  1295. # "B": float(match.group(5)),
  1296. # "H": float(match.group(6)),
  1297. # "Z": float(match.group(7))
  1298. }
  1299. self.tools[name] = spec
  1300. continue
  1301. ## Units and number format ##
  1302. match = self.units_re.match(eline)
  1303. if match:
  1304. self.zeros = match.group(2) # "T" or "L"
  1305. self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  1306. continue
  1307. print "WARNING: Line ignored:", eline
  1308. def create_geometry(self):
  1309. """
  1310. Creates circles of the tool diameter at every point
  1311. specified in ``self.drills``.
  1312. :return: None
  1313. """
  1314. self.solid_geometry = []
  1315. for drill in self.drills:
  1316. #poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
  1317. tooldia = self.tools[drill['tool']]['C']
  1318. poly = drill['point'].buffer(tooldia/2.0)
  1319. self.solid_geometry.append(poly)
  1320. def scale(self, factor):
  1321. """
  1322. Scales geometry on the XY plane in the object by a given factor.
  1323. Tool sizes, feedrates an Z-plane dimensions are untouched.
  1324. :param factor: Number by which to scale the object.
  1325. :type factor: float
  1326. :return: None
  1327. :rtype: NOne
  1328. """
  1329. # Drills
  1330. for drill in self.drills:
  1331. drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
  1332. self.create_geometry()
  1333. def offset(self, vect):
  1334. """
  1335. Offsets geometry on the XY plane in the object by a given vector.
  1336. :param vect: (x, y) offset vector.
  1337. :type vect: tuple
  1338. :return: None
  1339. """
  1340. dx, dy = vect
  1341. # Drills
  1342. for drill in self.drills:
  1343. drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
  1344. # Recreate geometry
  1345. self.create_geometry()
  1346. def mirror(self, axis, point):
  1347. """
  1348. :param axis: "X" or "Y" indicates around which axis to mirror.
  1349. :type axis: str
  1350. :param point: [x, y] point belonging to the mirror axis.
  1351. :type point: list
  1352. :return: None
  1353. """
  1354. px, py = point
  1355. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1356. # Modify data
  1357. for drill in self.drills:
  1358. drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
  1359. # Recreate geometry
  1360. self.create_geometry()
  1361. def convert_units(self, units):
  1362. factor = Geometry.convert_units(self, units)
  1363. # Tools
  1364. for tname in self.tools:
  1365. self.tools[tname]["C"] *= factor
  1366. self.create_geometry()
  1367. return factor
  1368. class CNCjob(Geometry):
  1369. """
  1370. Represents work to be done by a CNC machine.
  1371. *ATTRIBUTES*
  1372. * ``gcode_parsed`` (list): Each is a dictionary:
  1373. ===================== =========================================
  1374. Key Value
  1375. ===================== =========================================
  1376. geom (Shapely.LineString) Tool path (XY plane)
  1377. kind (string) "AB", A is "T" (travel) or
  1378. "C" (cut). B is "F" (fast) or "S" (slow).
  1379. ===================== =========================================
  1380. """
  1381. def __init__(self, units="in", kind="generic", z_move=0.1,
  1382. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  1383. Geometry.__init__(self)
  1384. self.kind = kind
  1385. self.units = units
  1386. self.z_cut = z_cut
  1387. self.z_move = z_move
  1388. self.feedrate = feedrate
  1389. self.tooldia = tooldia
  1390. self.unitcode = {"IN": "G20", "MM": "G21"}
  1391. self.pausecode = "G04 P1"
  1392. self.feedminutecode = "G94"
  1393. self.absolutecode = "G90"
  1394. self.gcode = ""
  1395. self.input_geometry_bounds = None
  1396. self.gcode_parsed = None
  1397. self.steps_per_circ = 20 # Used when parsing G-code arcs
  1398. # Attributes to be included in serialization
  1399. # Always append to it because it carries contents
  1400. # from Geometry.
  1401. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia',
  1402. 'gcode', 'input_geometry_bounds', 'gcode_parsed',
  1403. 'steps_per_circ']
  1404. def convert_units(self, units):
  1405. factor = Geometry.convert_units(self, units)
  1406. print "CNCjob.convert_units()"
  1407. self.z_cut *= factor
  1408. self.z_move *= factor
  1409. self.feedrate *= factor
  1410. self.tooldia *= factor
  1411. return factor
  1412. def generate_from_excellon(self, exobj):
  1413. """
  1414. Generates G-code for drilling from Excellon object.
  1415. self.gcode becomes a list, each element is a
  1416. different job for each tool in the excellon code.
  1417. """
  1418. self.kind = "drill"
  1419. self.gcode = []
  1420. t = "G00 X%.4fY%.4f\n"
  1421. down = "G01 Z%.4f\n" % self.z_cut
  1422. up = "G01 Z%.4f\n" % self.z_move
  1423. for tool in exobj.tools:
  1424. points = []
  1425. for drill in exobj.drill:
  1426. if drill['tool'] == tool:
  1427. points.append(drill['point'])
  1428. gcode = self.unitcode[self.units.upper()] + "\n"
  1429. gcode += self.absolutecode + "\n"
  1430. gcode += self.feedminutecode + "\n"
  1431. gcode += "F%.2f\n" % self.feedrate
  1432. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1433. gcode += "M03\n" # Spindle start
  1434. gcode += self.pausecode + "\n"
  1435. for point in points:
  1436. gcode += t % point
  1437. gcode += down + up
  1438. gcode += t % (0, 0)
  1439. gcode += "M05\n" # Spindle stop
  1440. self.gcode.append(gcode)
  1441. def generate_from_excellon_by_tool(self, exobj, tools="all"):
  1442. """
  1443. Creates gcode for this object from an Excellon object
  1444. for the specified tools.
  1445. :param exobj: Excellon object to process
  1446. :type exobj: Excellon
  1447. :param tools: Comma separated tool names
  1448. :type: tools: str
  1449. :return: None
  1450. :rtype: None
  1451. """
  1452. print "Creating CNC Job from Excellon..."
  1453. if tools == "all":
  1454. tools = [tool for tool in exobj.tools]
  1455. else:
  1456. tools = [x.strip() for x in tools.split(",")]
  1457. tools = filter(lambda i: i in exobj.tools, tools)
  1458. print "Tools are:", tools
  1459. points = []
  1460. for drill in exobj.drills:
  1461. if drill['tool'] in tools:
  1462. points.append(drill['point'])
  1463. print "Found %d drills." % len(points)
  1464. #self.kind = "drill"
  1465. self.gcode = []
  1466. t = "G00 X%.4fY%.4f\n"
  1467. down = "G01 Z%.4f\n" % self.z_cut
  1468. up = "G01 Z%.4f\n" % self.z_move
  1469. gcode = self.unitcode[self.units.upper()] + "\n"
  1470. gcode += self.absolutecode + "\n"
  1471. gcode += self.feedminutecode + "\n"
  1472. gcode += "F%.2f\n" % self.feedrate
  1473. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1474. gcode += "M03\n" # Spindle start
  1475. gcode += self.pausecode + "\n"
  1476. for point in points:
  1477. x, y = point.coords.xy
  1478. gcode += t % (x[0], y[0])
  1479. gcode += down + up
  1480. gcode += t % (0, 0)
  1481. gcode += "M05\n" # Spindle stop
  1482. self.gcode = gcode
  1483. def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0):
  1484. """
  1485. Generates G-Code from a Geometry object. Stores in ``self.gcode``.
  1486. :param geometry: Geometry defining the toolpath
  1487. :type geometry: Geometry
  1488. :param append: Wether to append to self.gcode or re-write it.
  1489. :type append: bool
  1490. :param tooldia: If given, sets the tooldia property but does
  1491. not affect the process in any other way.
  1492. :type tooldia: bool
  1493. :param tolerance: All points in the simplified object will be within the
  1494. tolerance distance of the original geometry.
  1495. :return: None
  1496. :rtype: None
  1497. """
  1498. if tooldia is not None:
  1499. self.tooldia = tooldia
  1500. self.input_geometry_bounds = geometry.bounds()
  1501. if not append:
  1502. self.gcode = ""
  1503. self.gcode = self.unitcode[self.units.upper()] + "\n"
  1504. self.gcode += self.absolutecode + "\n"
  1505. self.gcode += self.feedminutecode + "\n"
  1506. self.gcode += "F%.2f\n" % self.feedrate
  1507. self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1508. self.gcode += "M03\n" # Spindle start
  1509. self.gcode += self.pausecode + "\n"
  1510. for geo in geometry.solid_geometry:
  1511. if type(geo) == Polygon:
  1512. self.gcode += self.polygon2gcode(geo, tolerance=tolerance)
  1513. continue
  1514. if type(geo) == LineString or type(geo) == LinearRing:
  1515. self.gcode += self.linear2gcode(geo, tolerance=tolerance)
  1516. continue
  1517. if type(geo) == Point:
  1518. self.gcode += self.point2gcode(geo)
  1519. continue
  1520. if type(geo) == MultiPolygon:
  1521. for poly in geo:
  1522. self.gcode += self.polygon2gcode(poly, tolerance=tolerance)
  1523. continue
  1524. print "WARNING: G-code generation not implemented for %s" % (str(type(geo)))
  1525. self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1526. self.gcode += "G00 X0Y0\n"
  1527. self.gcode += "M05\n" # Spindle stop
  1528. def pre_parse(self, gtext):
  1529. """
  1530. Separates parts of the G-Code text into a list of dictionaries.
  1531. Used by ``self.gcode_parse()``.
  1532. :param gtext: A single string with g-code
  1533. """
  1534. # Units: G20-inches, G21-mm
  1535. units_re = re.compile(r'^G2([01])')
  1536. # TODO: This has to be re-done
  1537. gcmds = []
  1538. lines = gtext.split("\n") # TODO: This is probably a lot of work!
  1539. for line in lines:
  1540. # Clean up
  1541. line = line.strip()
  1542. # Remove comments
  1543. # NOTE: Limited to 1 bracket pair
  1544. op = line.find("(")
  1545. cl = line.find(")")
  1546. #if op > -1 and cl > op:
  1547. if cl > op > -1:
  1548. #comment = line[op+1:cl]
  1549. line = line[:op] + line[(cl+1):]
  1550. # Units
  1551. match = units_re.match(line)
  1552. if match:
  1553. self.units = {'0': "IN", '1': "MM"}[match.group(1)]
  1554. # Parse GCode
  1555. # 0 4 12
  1556. # G01 X-0.007 Y-0.057
  1557. # --> codes_idx = [0, 4, 12]
  1558. codes = "NMGXYZIJFP"
  1559. codes_idx = []
  1560. i = 0
  1561. for ch in line:
  1562. if ch in codes:
  1563. codes_idx.append(i)
  1564. i += 1
  1565. n_codes = len(codes_idx)
  1566. if n_codes == 0:
  1567. continue
  1568. # Separate codes in line
  1569. parts = []
  1570. for p in range(n_codes-1):
  1571. parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
  1572. parts.append(line[codes_idx[-1]:].strip())
  1573. # Separate codes from values
  1574. cmds = {}
  1575. for part in parts:
  1576. cmds[part[0]] = float(part[1:])
  1577. gcmds.append(cmds)
  1578. return gcmds
  1579. def gcode_parse(self):
  1580. """
  1581. G-Code parser (from self.gcode). Generates dictionary with
  1582. single-segment LineString's and "kind" indicating cut or travel,
  1583. fast or feedrate speed.
  1584. """
  1585. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  1586. # Results go here
  1587. geometry = []
  1588. # TODO: Merge into single parser?
  1589. gobjs = self.pre_parse(self.gcode)
  1590. # Last known instruction
  1591. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  1592. # Current path: temporary storage until tool is
  1593. # lifted or lowered.
  1594. path = [(0, 0)]
  1595. # Process every instruction
  1596. for gobj in gobjs:
  1597. ## Changing height
  1598. if 'Z' in gobj:
  1599. if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  1600. print "WARNING: Non-orthogonal motion: From", current
  1601. print " To:", gobj
  1602. current['Z'] = gobj['Z']
  1603. # Store the path into geometry and reset path
  1604. if len(path) > 1:
  1605. geometry.append({"geom": LineString(path),
  1606. "kind": kind})
  1607. path = [path[-1]] # Start with the last point of last path.
  1608. if 'G' in gobj:
  1609. current['G'] = int(gobj['G'])
  1610. if 'X' in gobj or 'Y' in gobj:
  1611. if 'X' in gobj:
  1612. x = gobj['X']
  1613. else:
  1614. x = current['X']
  1615. if 'Y' in gobj:
  1616. y = gobj['Y']
  1617. else:
  1618. y = current['Y']
  1619. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  1620. if current['Z'] > 0:
  1621. kind[0] = 'T'
  1622. if current['G'] > 0:
  1623. kind[1] = 'S'
  1624. arcdir = [None, None, "cw", "ccw"]
  1625. if current['G'] in [0, 1]: # line
  1626. path.append((x, y))
  1627. if current['G'] in [2, 3]: # arc
  1628. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  1629. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  1630. start = arctan2(-gobj['J'], -gobj['I'])
  1631. stop = arctan2(-center[1]+y, -center[0]+x)
  1632. path += arc(center, radius, start, stop,
  1633. arcdir[current['G']],
  1634. self.steps_per_circ)
  1635. # Update current instruction
  1636. for code in gobj:
  1637. current[code] = gobj[code]
  1638. # There might not be a change in height at the
  1639. # end, therefore, see here too if there is
  1640. # a final path.
  1641. if len(path) > 1:
  1642. geometry.append({"geom": LineString(path),
  1643. "kind": kind})
  1644. self.gcode_parsed = geometry
  1645. return geometry
  1646. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  1647. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  1648. # alpha={"T": 0.3, "C": 1.0}):
  1649. # """
  1650. # Creates a Matplotlib figure with a plot of the
  1651. # G-code job.
  1652. # """
  1653. # if tooldia is None:
  1654. # tooldia = self.tooldia
  1655. #
  1656. # fig = Figure(dpi=dpi)
  1657. # ax = fig.add_subplot(111)
  1658. # ax.set_aspect(1)
  1659. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  1660. # ax.set_xlim(xmin-margin, xmax+margin)
  1661. # ax.set_ylim(ymin-margin, ymax+margin)
  1662. #
  1663. # if tooldia == 0:
  1664. # for geo in self.gcode_parsed:
  1665. # linespec = '--'
  1666. # linecolor = color[geo['kind'][0]][1]
  1667. # if geo['kind'][0] == 'C':
  1668. # linespec = 'k-'
  1669. # x, y = geo['geom'].coords.xy
  1670. # ax.plot(x, y, linespec, color=linecolor)
  1671. # else:
  1672. # for geo in self.gcode_parsed:
  1673. # poly = geo['geom'].buffer(tooldia/2.0)
  1674. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  1675. # edgecolor=color[geo['kind'][0]][1],
  1676. # alpha=alpha[geo['kind'][0]], zorder=2)
  1677. # ax.add_patch(patch)
  1678. #
  1679. # return fig
  1680. def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
  1681. color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  1682. alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
  1683. """
  1684. Plots the G-code job onto the given axes.
  1685. :param axes: Matplotlib axes on which to plot.
  1686. :param tooldia: Tool diameter.
  1687. :param dpi: Not used!
  1688. :param margin: Not used!
  1689. :param color: Color specification.
  1690. :param alpha: Transparency specification.
  1691. :param tool_tolerance: Tolerance when drawing the toolshape.
  1692. :return: None
  1693. """
  1694. if tooldia is None:
  1695. tooldia = self.tooldia
  1696. if tooldia == 0:
  1697. for geo in self.gcode_parsed:
  1698. linespec = '--'
  1699. linecolor = color[geo['kind'][0]][1]
  1700. if geo['kind'][0] == 'C':
  1701. linespec = 'k-'
  1702. x, y = geo['geom'].coords.xy
  1703. axes.plot(x, y, linespec, color=linecolor)
  1704. else:
  1705. for geo in self.gcode_parsed:
  1706. poly = geo['geom'].buffer(tooldia/2.0).simplify(tool_tolerance)
  1707. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  1708. edgecolor=color[geo['kind'][0]][1],
  1709. alpha=alpha[geo['kind'][0]], zorder=2)
  1710. axes.add_patch(patch)
  1711. def create_geometry(self):
  1712. # TODO: This takes forever. Too much data?
  1713. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  1714. def polygon2gcode(self, polygon, tolerance=0):
  1715. """
  1716. Creates G-Code for the exterior and all interior paths
  1717. of a polygon.
  1718. :param polygon: A Shapely.Polygon
  1719. :type polygon: Shapely.Polygon
  1720. :param tolerance: All points in the simplified object will be within the
  1721. tolerance distance of the original geometry.
  1722. :type tolerance: float
  1723. :return: G-code to cut along polygon.
  1724. :rtype: str
  1725. """
  1726. if tolerance > 0:
  1727. target_polygon = polygon.simplify(tolerance)
  1728. else:
  1729. target_polygon = polygon
  1730. gcode = ""
  1731. t = "G0%d X%.4fY%.4f\n"
  1732. path = list(target_polygon.exterior.coords) # Polygon exterior
  1733. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1734. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1735. for pt in path[1:]:
  1736. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1737. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1738. for ints in target_polygon.interiors: # Polygon interiors
  1739. path = list(ints.coords)
  1740. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1741. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1742. for pt in path[1:]:
  1743. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1744. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1745. return gcode
  1746. def linear2gcode(self, linear, tolerance=0):
  1747. """
  1748. Generates G-code to cut along the linear feature.
  1749. :param linear: The path to cut along.
  1750. :type: Shapely.LinearRing or Shapely.Linear String
  1751. :param tolerance: All points in the simplified object will be within the
  1752. tolerance distance of the original geometry.
  1753. :type tolerance: float
  1754. :return: G-code to cut alon the linear feature.
  1755. :rtype: str
  1756. """
  1757. if tolerance > 0:
  1758. target_linear = linear.simplify(tolerance)
  1759. else:
  1760. target_linear = linear
  1761. gcode = ""
  1762. t = "G0%d X%.4fY%.4f\n"
  1763. path = list(target_linear.coords)
  1764. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1765. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1766. for pt in path[1:]:
  1767. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1768. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1769. return gcode
  1770. def point2gcode(self, point):
  1771. # TODO: This is not doing anything.
  1772. gcode = ""
  1773. t = "G0%d X%.4fY%.4f\n"
  1774. path = list(point.coords)
  1775. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1776. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1777. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1778. def scale(self, factor):
  1779. """
  1780. Scales all the geometry on the XY plane in the object by the
  1781. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  1782. not altered.
  1783. :param factor: Number by which to scale the object.
  1784. :type factor: float
  1785. :return: None
  1786. :rtype: None
  1787. """
  1788. for g in self.gcode_parsed:
  1789. g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1790. self.create_geometry()
  1791. def offset(self, vect):
  1792. """
  1793. Offsets all the geometry on the XY plane in the object by the
  1794. given vector.
  1795. :param vect: (x, y) offset vector.
  1796. :type vect: tuple
  1797. :return: None
  1798. """
  1799. dx, dy = vect
  1800. for g in self.gcode_parsed:
  1801. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  1802. self.create_geometry()
  1803. # def get_bounds(geometry_set):
  1804. # xmin = Inf
  1805. # ymin = Inf
  1806. # xmax = -Inf
  1807. # ymax = -Inf
  1808. #
  1809. # #print "Getting bounds of:", str(geometry_set)
  1810. # for gs in geometry_set:
  1811. # try:
  1812. # gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
  1813. # xmin = min([xmin, gxmin])
  1814. # ymin = min([ymin, gymin])
  1815. # xmax = max([xmax, gxmax])
  1816. # ymax = max([ymax, gymax])
  1817. # except:
  1818. # print "DEV WARNING: Tried to get bounds of empty geometry."
  1819. #
  1820. # return [xmin, ymin, xmax, ymax]
  1821. def get_bounds(geometry_list):
  1822. xmin = Inf
  1823. ymin = Inf
  1824. xmax = -Inf
  1825. ymax = -Inf
  1826. #print "Getting bounds of:", str(geometry_set)
  1827. for gs in geometry_list:
  1828. try:
  1829. gxmin, gymin, gxmax, gymax = gs.bounds()
  1830. xmin = min([xmin, gxmin])
  1831. ymin = min([ymin, gymin])
  1832. xmax = max([xmax, gxmax])
  1833. ymax = max([ymax, gymax])
  1834. except:
  1835. print "DEV WARNING: Tried to get bounds of empty geometry."
  1836. return [xmin, ymin, xmax, ymax]
  1837. def arc(center, radius, start, stop, direction, steps_per_circ):
  1838. """
  1839. Creates a list of point along the specified arc.
  1840. :param center: Coordinates of the center [x, y]
  1841. :type center: list
  1842. :param radius: Radius of the arc.
  1843. :type radius: float
  1844. :param start: Starting angle in radians
  1845. :type start: float
  1846. :param stop: End angle in radians
  1847. :type stop: float
  1848. :param direction: Orientation of the arc, "CW" or "CCW"
  1849. :type direction: string
  1850. :param steps_per_circ: Number of straight line segments to
  1851. represent a circle.
  1852. :type steps_per_circ: int
  1853. :return: The desired arc, as list of tuples
  1854. :rtype: list
  1855. """
  1856. # TODO: Resolution should be established by fraction of total length, not angle.
  1857. da_sign = {"cw": -1.0, "ccw": 1.0}
  1858. points = []
  1859. if direction == "ccw" and stop <= start:
  1860. stop += 2*pi
  1861. if direction == "cw" and stop >= start:
  1862. stop -= 2*pi
  1863. angle = abs(stop - start)
  1864. #angle = stop-start
  1865. steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2])
  1866. delta_angle = da_sign[direction]*angle*1.0/steps
  1867. for i in range(steps+1):
  1868. theta = start + delta_angle*i
  1869. points.append((center[0]+radius*cos(theta), center[1]+radius*sin(theta)))
  1870. return points
  1871. def clear_poly(poly, tooldia, overlap=0.1):
  1872. """
  1873. Creates a list of Shapely geometry objects covering the inside
  1874. of a Shapely.Polygon. Use for removing all the copper in a region
  1875. or bed flattening.
  1876. :param poly: Target polygon
  1877. :type poly: Shapely.Polygon
  1878. :param tooldia: Diameter of the tool
  1879. :type tooldia: float
  1880. :param overlap: Fraction of the tool diameter to overlap
  1881. in each pass.
  1882. :type overlap: float
  1883. :return: list of Shapely.Polygon
  1884. :rtype: list
  1885. """
  1886. poly_cuts = [poly.buffer(-tooldia/2.0)]
  1887. while True:
  1888. poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  1889. if poly.area > 0:
  1890. poly_cuts.append(poly)
  1891. else:
  1892. break
  1893. return poly_cuts
  1894. def find_polygon(poly_set, point):
  1895. """
  1896. Return the first polygon in the list of polygons poly_set
  1897. that contains the given point.
  1898. """
  1899. p = Point(point)
  1900. for poly in poly_set:
  1901. if poly.contains(p):
  1902. return poly
  1903. return None
  1904. def to_dict(obj):
  1905. """
  1906. Makes a Shapely geometry object into serializeable form.
  1907. :param obj: Shapely geometry.
  1908. :type obj: BaseGeometry
  1909. :return: Dictionary with serializable form if ``obj`` was
  1910. BaseGeometry or ApertureMacro, otherwise returns ``obj``.
  1911. """
  1912. if isinstance(obj, ApertureMacro):
  1913. return {
  1914. "__class__": "ApertureMacro",
  1915. "__inst__": obj.to_dict()
  1916. }
  1917. if isinstance(obj, BaseGeometry):
  1918. return {
  1919. "__class__": "Shply",
  1920. "__inst__": sdumps(obj)
  1921. }
  1922. return obj
  1923. def dict2obj(d):
  1924. """
  1925. Default deserializer.
  1926. :param d: Serializable dictionary representation of an object
  1927. to be reconstructed.
  1928. :return: Reconstructed object.
  1929. """
  1930. if '__class__' in d and '__inst__' in d:
  1931. if d['__class__'] == "Shply":
  1932. return sloads(d['__inst__'])
  1933. if d['__class__'] == "ApertureMacro":
  1934. am = ApertureMacro()
  1935. am.from_dict(d['__inst__'])
  1936. return am
  1937. return d
  1938. else:
  1939. return d
  1940. def plotg(geo):
  1941. try:
  1942. _ = iter(geo)
  1943. except:
  1944. geo = [geo]
  1945. for g in geo:
  1946. if type(g) == Polygon:
  1947. x, y = g.exterior.coords.xy
  1948. plot(x, y)
  1949. for ints in g.interiors:
  1950. x, y = ints.coords.xy
  1951. plot(x, y)
  1952. continue
  1953. if type(g) == LineString or type(g) == LinearRing:
  1954. x, y = g.coords.xy
  1955. plot(x, y)
  1956. continue
  1957. if type(g) == Point:
  1958. x, y = g.coords.xy
  1959. plot(x, y, 'o')
  1960. continue
  1961. try:
  1962. _ = iter(g)
  1963. plotg(g)
  1964. except:
  1965. print "Cannot plot:", str(type(g))
  1966. continue
  1967. def parse_gerber_number(strnumber, frac_digits):
  1968. """
  1969. Parse a single number of Gerber coordinates.
  1970. :param strnumber: String containing a number in decimal digits
  1971. from a coordinate data block, possibly with a leading sign.
  1972. :type strnumber: str
  1973. :param frac_digits: Number of digits used for the fractional
  1974. part of the number
  1975. :type frac_digits: int
  1976. :return: The number in floating point.
  1977. :rtype: float
  1978. """
  1979. return int(strnumber)*(10**(-frac_digits))
  1980. def parse_gerber_coords(gstr, int_digits, frac_digits):
  1981. """
  1982. Parse Gerber coordinates
  1983. :param gstr: Line of G-Code containing coordinates.
  1984. :type gstr: str
  1985. :param int_digits: Number of digits in integer part of a number.
  1986. :type int_digits: int
  1987. :param frac_digits: Number of digits in frac_digits part of a number.
  1988. :type frac_digits: int
  1989. :return: [x, y] coordinates.
  1990. :rtype: list
  1991. """
  1992. global gerbx, gerby
  1993. xindex = gstr.find("X")
  1994. yindex = gstr.find("Y")
  1995. index = gstr.find("D")
  1996. if xindex == -1:
  1997. x = gerbx
  1998. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  1999. elif yindex == -1:
  2000. y = gerby
  2001. x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
  2002. else:
  2003. x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
  2004. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  2005. gerbx = x
  2006. gerby = y
  2007. return [x, y]