ParseGerber.py 95 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171
  1. from camlib import Geometry, arc, arc_angle, ApertureMacro
  2. import FlatCAMApp
  3. import numpy as np
  4. import re
  5. import logging
  6. import traceback
  7. from copy import deepcopy
  8. import sys
  9. from shapely.ops import cascaded_union, unary_union
  10. from shapely.geometry import Polygon, MultiPolygon, LineString, Point
  11. import shapely.affinity as affinity
  12. from shapely.geometry import box as shply_box
  13. from lxml import etree as ET
  14. from flatcamParsers.ParseSVG import *
  15. import FlatCAMTranslation as fcTranslate
  16. import gettext
  17. import builtins
  18. if '_' not in builtins.__dict__:
  19. _ = gettext.gettext
  20. log = logging.getLogger('base')
  21. class Gerber(Geometry):
  22. """
  23. Here it is done all the Gerber parsing.
  24. **ATTRIBUTES**
  25. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  26. The values are dictionaries key/value pairs which describe the aperture. The
  27. type key is always present and the rest depend on the key:
  28. +-----------+-----------------------------------+
  29. | Key | Value |
  30. +===========+===================================+
  31. | type | (str) "C", "R", "O", "P", or "AP" |
  32. +-----------+-----------------------------------+
  33. | others | Depend on ``type`` |
  34. +-----------+-----------------------------------+
  35. | solid_geometry | (list) |
  36. +-----------+-----------------------------------+
  37. * ``aperture_macros`` (dictionary): Are predefined geometrical structures
  38. that can be instantiated with different parameters in an aperture
  39. definition. See ``apertures`` above. The key is the name of the macro,
  40. and the macro itself, the value, is a ``Aperture_Macro`` object.
  41. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  42. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  43. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  44. *buffering* (or thickening) the ``paths`` with the aperture. These are
  45. generated from ``paths`` in ``buffer_paths()``.
  46. **USAGE**::
  47. g = Gerber()
  48. g.parse_file(filename)
  49. g.create_geometry()
  50. do_something(s.solid_geometry)
  51. """
  52. # defaults = {
  53. # "steps_per_circle": 128,
  54. # "use_buffer_for_union": True
  55. # }
  56. def __init__(self, steps_per_circle=None):
  57. """
  58. The constructor takes no parameters. Use ``gerber.parse_files()``
  59. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  60. :return: Gerber object
  61. :rtype: Gerber
  62. """
  63. # How to approximate a circle with lines.
  64. self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
  65. # Initialize parent
  66. Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
  67. # Number format
  68. self.int_digits = 3
  69. """Number of integer digits in Gerber numbers. Used during parsing."""
  70. self.frac_digits = 4
  71. """Number of fraction digits in Gerber numbers. Used during parsing."""
  72. self.gerber_zeros = self.app.defaults['gerber_def_zeros']
  73. """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
  74. """
  75. # ## Gerber elements # ##
  76. '''
  77. apertures = {
  78. 'id':{
  79. 'type':string,
  80. 'size':float,
  81. 'width':float,
  82. 'height':float,
  83. 'geometry': [],
  84. }
  85. }
  86. apertures['geometry'] list elements are dicts
  87. dict = {
  88. 'solid': [],
  89. 'follow': [],
  90. 'clear': []
  91. }
  92. '''
  93. # store the file units here:
  94. self.units = self.app.defaults['gerber_def_units']
  95. # aperture storage
  96. self.apertures = {}
  97. # Aperture Macros
  98. self.aperture_macros = {}
  99. # will store the Gerber geometry's as solids
  100. self.solid_geometry = Polygon()
  101. # will store the Gerber geometry's as paths
  102. self.follow_geometry = []
  103. # made True when the LPC command is encountered in Gerber parsing
  104. # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
  105. self.is_lpc = False
  106. self.source_file = ''
  107. # Attributes to be included in serialization
  108. # Always append to it because it carries contents
  109. # from Geometry.
  110. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
  111. 'aperture_macros', 'solid_geometry', 'source_file']
  112. # ### Parser patterns ## ##
  113. # FS - Format Specification
  114. # The format of X and Y must be the same!
  115. # L-omit leading zeros, T-omit trailing zeros, D-no zero supression
  116. # A-absolute notation, I-incremental notation
  117. self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\d\*%?$')
  118. self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
  119. self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
  120. # Mode (IN/MM)
  121. self.mode_re = re.compile(r'^%?MO(IN|MM)\*%?$')
  122. # Comment G04|G4
  123. self.comm_re = re.compile(r'^G0?4(.*)$')
  124. # AD - Aperture definition
  125. # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
  126. # NOTE: Adding "-" to support output from Upverter.
  127. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
  128. # AM - Aperture Macro
  129. # Beginning of macro (Ends with *%):
  130. # self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  131. # Tool change
  132. # May begin with G54 but that is deprecated
  133. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  134. # G01... - Linear interpolation plus flashes with coordinates
  135. # Operation code (D0x) missing is deprecated... oh well I will support it.
  136. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  137. # Operation code alone, usually just D03 (Flash)
  138. self.opcode_re = re.compile(r'^D0?([123])\*$')
  139. # G02/3... - Circular interpolation with coordinates
  140. # 2-clockwise, 3-counterclockwise
  141. # Operation code (D0x) missing is deprecated... oh well I will support it.
  142. # Optional start with G02 or G03, optional end with D01 or D02 with
  143. # optional coordinates but at least one in any order.
  144. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' +
  145. '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  146. # G01/2/3 Occurring without coordinates
  147. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  148. # Single G74 or multi G75 quadrant for circular interpolation
  149. self.quad_re = re.compile(r'^G7([45]).*\*$')
  150. # Region mode on
  151. # In region mode, D01 starts a region
  152. # and D02 ends it. A new region can be started again
  153. # with D01. All contours must be closed before
  154. # D02 or G37.
  155. self.regionon_re = re.compile(r'^G36\*$')
  156. # Region mode off
  157. # Will end a region and come off region mode.
  158. # All contours must be closed before D02 or G37.
  159. self.regionoff_re = re.compile(r'^G37\*$')
  160. # End of file
  161. self.eof_re = re.compile(r'^M02\*')
  162. # IP - Image polarity
  163. self.pol_re = re.compile(r'^%?IP(POS|NEG)\*%?$')
  164. # LP - Level polarity
  165. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  166. # Units (OBSOLETE)
  167. self.units_re = re.compile(r'^G7([01])\*$')
  168. # Absolute/Relative G90/1 (OBSOLETE)
  169. self.absrel_re = re.compile(r'^G9([01])\*$')
  170. # Aperture macros
  171. self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
  172. self.am2_re = re.compile(r'(.*)%$')
  173. # flag to store if a conversion was done. It is needed because multiple units declarations can be found
  174. # in a Gerber file (normal or obsolete ones)
  175. self.conversion_done = False
  176. self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
  177. def aperture_parse(self, apertureId, apertureType, apParameters):
  178. """
  179. Parse gerber aperture definition into dictionary of apertures.
  180. The following kinds and their attributes are supported:
  181. * *Circular (C)*: size (float)
  182. * *Rectangle (R)*: width (float), height (float)
  183. * *Obround (O)*: width (float), height (float).
  184. * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
  185. * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
  186. :param apertureId: Id of the aperture being defined.
  187. :param apertureType: Type of the aperture.
  188. :param apParameters: Parameters of the aperture.
  189. :type apertureId: str
  190. :type apertureType: str
  191. :type apParameters: str
  192. :return: Identifier of the aperture.
  193. :rtype: str
  194. """
  195. if self.app.abort_flag:
  196. # graceful abort requested by the user
  197. raise FlatCAMApp.GracefulException
  198. # Found some Gerber with a leading zero in the aperture id and the
  199. # referenced it without the zero, so this is a hack to handle that.
  200. apid = str(int(apertureId))
  201. try: # Could be empty for aperture macros
  202. paramList = apParameters.split('X')
  203. except Exception:
  204. paramList = None
  205. if apertureType == "C": # Circle, example: %ADD11C,0.1*%
  206. self.apertures[apid] = {"type": "C",
  207. "size": float(paramList[0])}
  208. return apid
  209. if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*%
  210. self.apertures[apid] = {"type": "R",
  211. "width": float(paramList[0]),
  212. "height": float(paramList[1]),
  213. "size": np.sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)} # Hack
  214. return apid
  215. if apertureType == "O": # Obround
  216. self.apertures[apid] = {"type": "O",
  217. "width": float(paramList[0]),
  218. "height": float(paramList[1]),
  219. "size": np.sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)} # Hack
  220. return apid
  221. if apertureType == "P": # Polygon (regular)
  222. self.apertures[apid] = {"type": "P",
  223. "diam": float(paramList[0]),
  224. "nVertices": int(paramList[1]),
  225. "size": float(paramList[0])} # Hack
  226. if len(paramList) >= 3:
  227. self.apertures[apid]["rotation"] = float(paramList[2])
  228. return apid
  229. if apertureType in self.aperture_macros:
  230. self.apertures[apid] = {"type": "AM",
  231. "macro": self.aperture_macros[apertureType],
  232. "modifiers": paramList}
  233. return apid
  234. log.warning("Aperture not implemented: %s" % str(apertureType))
  235. return None
  236. def parse_file(self, filename, follow=False):
  237. """
  238. Calls Gerber.parse_lines() with generator of lines
  239. read from the given file. Will split the lines if multiple
  240. statements are found in a single original line.
  241. The following line is split into two::
  242. G54D11*G36*
  243. First is ``G54D11*`` and seconds is ``G36*``.
  244. :param filename: Gerber file to parse.
  245. :type filename: str
  246. :param follow: If true, will not create polygons, just lines
  247. following the gerber path.
  248. :type follow: bool
  249. :return: None
  250. """
  251. with open(filename, 'r') as gfile:
  252. def line_generator():
  253. for line in gfile:
  254. line = line.strip(' \r\n')
  255. while len(line) > 0:
  256. # If ends with '%' leave as is.
  257. if line[-1] == '%':
  258. yield line
  259. break
  260. # Split after '*' if any.
  261. starpos = line.find('*')
  262. if starpos > -1:
  263. cleanline = line[:starpos + 1]
  264. yield cleanline
  265. line = line[starpos + 1:]
  266. # Otherwise leave as is.
  267. else:
  268. # yield clean line
  269. yield line
  270. break
  271. processed_lines = list(line_generator())
  272. self.parse_lines(processed_lines)
  273. # @profile
  274. def parse_lines(self, glines):
  275. """
  276. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  277. ``self.flashes``, ``self.regions`` and ``self.units``.
  278. :param glines: Gerber code as list of strings, each element being
  279. one line of the source file.
  280. :type glines: list
  281. :return: None
  282. :rtype: None
  283. """
  284. # Coordinates of the current path, each is [x, y]
  285. path = []
  286. # this is for temporary storage of solid geometry until it is added to poly_buffer
  287. geo_s = None
  288. # this is for temporary storage of follow geometry until it is added to follow_buffer
  289. geo_f = None
  290. # Polygons are stored here until there is a change in polarity.
  291. # Only then they are combined via cascaded_union and added or
  292. # subtracted from solid_geometry. This is ~100 times faster than
  293. # applying a union for every new polygon.
  294. poly_buffer = []
  295. # store here the follow geometry
  296. follow_buffer = []
  297. last_path_aperture = None
  298. current_aperture = None
  299. # 1,2 or 3 from "G01", "G02" or "G03"
  300. current_interpolation_mode = None
  301. # 1 or 2 from "D01" or "D02"
  302. # Note this is to support deprecated Gerber not putting
  303. # an operation code at the end of every coordinate line.
  304. current_operation_code = None
  305. # Current coordinates
  306. current_x = None
  307. current_y = None
  308. previous_x = None
  309. previous_y = None
  310. current_d = None
  311. # Absolute or Relative/Incremental coordinates
  312. # Not implemented
  313. absolute = True
  314. # How to interpret circular interpolation: SINGLE or MULTI
  315. quadrant_mode = None
  316. # Indicates we are parsing an aperture macro
  317. current_macro = None
  318. # Indicates the current polarity: D-Dark, C-Clear
  319. current_polarity = 'D'
  320. # If a region is being defined
  321. making_region = False
  322. # ### Parsing starts here ## ##
  323. line_num = 0
  324. gline = ""
  325. s_tol = float(self.app.defaults["gerber_simp_tolerance"])
  326. self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
  327. try:
  328. for gline in glines:
  329. if self.app.abort_flag:
  330. # graceful abort requested by the user
  331. raise FlatCAMApp.GracefulException
  332. line_num += 1
  333. self.source_file += gline + '\n'
  334. # Cleanup #
  335. gline = gline.strip(' \r\n')
  336. # log.debug("Line=%3s %s" % (line_num, gline))
  337. # ###################
  338. # Ignored lines #####
  339. # Comments #####
  340. # ###################
  341. match = self.comm_re.search(gline)
  342. if match:
  343. continue
  344. # Polarity change ###### ##
  345. # Example: %LPD*% or %LPC*%
  346. # If polarity changes, creates geometry from current
  347. # buffer, then adds or subtracts accordingly.
  348. match = self.lpol_re.search(gline)
  349. if match:
  350. new_polarity = match.group(1)
  351. # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
  352. self.is_lpc = True if new_polarity == 'C' else False
  353. if len(path) > 1 and current_polarity != new_polarity:
  354. # finish the current path and add it to the storage
  355. # --- Buffered ----
  356. width = self.apertures[last_path_aperture]["size"]
  357. geo_dict = dict()
  358. geo_f = LineString(path)
  359. if not geo_f.is_empty:
  360. follow_buffer.append(geo_f)
  361. geo_dict['follow'] = geo_f
  362. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  363. if not geo_s.is_empty:
  364. if self.app.defaults['gerber_simplification']:
  365. poly_buffer.append(geo_s.simplify(s_tol))
  366. else:
  367. poly_buffer.append(geo_s)
  368. if self.is_lpc is True:
  369. geo_dict['clear'] = geo_s
  370. else:
  371. geo_dict['solid'] = geo_s
  372. if last_path_aperture not in self.apertures:
  373. self.apertures[last_path_aperture] = dict()
  374. if 'geometry' not in self.apertures[last_path_aperture]:
  375. self.apertures[last_path_aperture]['geometry'] = []
  376. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  377. path = [path[-1]]
  378. # --- Apply buffer ---
  379. # If added for testing of bug #83
  380. # TODO: Remove when bug fixed
  381. if len(poly_buffer) > 0:
  382. if current_polarity == 'D':
  383. # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
  384. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  385. else:
  386. # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
  387. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  388. # follow_buffer = []
  389. poly_buffer = []
  390. current_polarity = new_polarity
  391. continue
  392. # ############################################################# ##
  393. # Number format ############################################### ##
  394. # Example: %FSLAX24Y24*%
  395. # ############################################################# ##
  396. # TODO: This is ignoring most of the format. Implement the rest.
  397. match = self.fmt_re.search(gline)
  398. if match:
  399. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
  400. if match.group(1) is not None:
  401. self.gerber_zeros = match.group(1)
  402. self.int_digits = int(match.group(3))
  403. self.frac_digits = int(match.group(4))
  404. log.debug("Gerber format found. (%s) " % str(gline))
  405. log.debug(
  406. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
  407. "D-no zero supression)" % self.gerber_zeros)
  408. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  409. continue
  410. # ## Mode (IN/MM)
  411. # Example: %MOIN*%
  412. match = self.mode_re.search(gline)
  413. if match:
  414. self.units = match.group(1)
  415. log.debug("Gerber units found = %s" % self.units)
  416. # Changed for issue #80
  417. # self.convert_units(match.group(1))
  418. self.conversion_done = True
  419. continue
  420. # ############################################################# ##
  421. # Combined Number format and Mode --- Allegro does this ####### ##
  422. # ############################################################# ##
  423. match = self.fmt_re_alt.search(gline)
  424. if match:
  425. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
  426. if match.group(1) is not None:
  427. self.gerber_zeros = match.group(1)
  428. self.int_digits = int(match.group(3))
  429. self.frac_digits = int(match.group(4))
  430. log.debug("Gerber format found. (%s) " % str(gline))
  431. log.debug(
  432. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
  433. "D-no zero suppression)" % self.gerber_zeros)
  434. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  435. self.units = match.group(5)
  436. log.debug("Gerber units found = %s" % self.units)
  437. # Changed for issue #80
  438. # self.convert_units(match.group(5))
  439. self.conversion_done = True
  440. continue
  441. # ############################################################# ##
  442. # Search for OrCAD way for having Number format
  443. # ############################################################# ##
  444. match = self.fmt_re_orcad.search(gline)
  445. if match:
  446. if match.group(1) is not None:
  447. if match.group(1) == 'G74':
  448. quadrant_mode = 'SINGLE'
  449. elif match.group(1) == 'G75':
  450. quadrant_mode = 'MULTI'
  451. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)]
  452. if match.group(2) is not None:
  453. self.gerber_zeros = match.group(2)
  454. self.int_digits = int(match.group(4))
  455. self.frac_digits = int(match.group(5))
  456. log.debug("Gerber format found. (%s) " % str(gline))
  457. log.debug(
  458. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
  459. "D-no zerosuppressionn)" % self.gerber_zeros)
  460. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  461. self.units = match.group(1)
  462. log.debug("Gerber units found = %s" % self.units)
  463. # Changed for issue #80
  464. # self.convert_units(match.group(5))
  465. self.conversion_done = True
  466. continue
  467. # ############################################################# ##
  468. # Units (G70/1) OBSOLETE
  469. # ############################################################# ##
  470. match = self.units_re.search(gline)
  471. if match:
  472. obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
  473. log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
  474. # Changed for issue #80
  475. # self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
  476. self.conversion_done = True
  477. continue
  478. # ############################################################# ##
  479. # Absolute/relative coordinates G90/1 OBSOLETE ######## ##
  480. # ##################################################### ##
  481. match = self.absrel_re.search(gline)
  482. if match:
  483. absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
  484. log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
  485. continue
  486. # ############################################################# ##
  487. # Aperture Macros ##################################### ##
  488. # Having this at the beginning will slow things down
  489. # but macros can have complicated statements than could
  490. # be caught by other patterns.
  491. # ############################################################# ##
  492. if current_macro is None: # No macro started yet
  493. match = self.am1_re.search(gline)
  494. # Start macro if match, else not an AM, carry on.
  495. if match:
  496. log.debug("Starting macro. Line %d: %s" % (line_num, gline))
  497. current_macro = match.group(1)
  498. self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
  499. if match.group(2): # Append
  500. self.aperture_macros[current_macro].append(match.group(2))
  501. if match.group(3): # Finish macro
  502. # self.aperture_macros[current_macro].parse_content()
  503. current_macro = None
  504. log.debug("Macro complete in 1 line.")
  505. continue
  506. else: # Continue macro
  507. log.debug("Continuing macro. Line %d." % line_num)
  508. match = self.am2_re.search(gline)
  509. if match: # Finish macro
  510. log.debug("End of macro. Line %d." % line_num)
  511. self.aperture_macros[current_macro].append(match.group(1))
  512. # self.aperture_macros[current_macro].parse_content()
  513. current_macro = None
  514. else: # Append
  515. self.aperture_macros[current_macro].append(gline)
  516. continue
  517. # ## Aperture definitions %ADD...
  518. match = self.ad_re.search(gline)
  519. if match:
  520. # log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
  521. self.aperture_parse(match.group(1), match.group(2), match.group(3))
  522. continue
  523. # ############################################################# ##
  524. # Operation code alone ###################### ##
  525. # Operation code alone, usually just D03 (Flash)
  526. # self.opcode_re = re.compile(r'^D0?([123])\*$')
  527. # ############################################################# ##
  528. match = self.opcode_re.search(gline)
  529. if match:
  530. current_operation_code = int(match.group(1))
  531. current_d = current_operation_code
  532. if current_operation_code == 3:
  533. # --- Buffered ---
  534. try:
  535. # log.debug("Bare op-code %d." % current_operation_code)
  536. geo_dict = dict()
  537. flash = self.create_flash_geometry(
  538. Point(current_x, current_y), self.apertures[current_aperture],
  539. self.steps_per_circle)
  540. geo_dict['follow'] = Point([current_x, current_y])
  541. if not flash.is_empty:
  542. if self.app.defaults['gerber_simplification']:
  543. poly_buffer.append(flash.simplify(s_tol))
  544. else:
  545. poly_buffer.append(flash)
  546. if self.is_lpc is True:
  547. geo_dict['clear'] = flash
  548. else:
  549. geo_dict['solid'] = flash
  550. if current_aperture not in self.apertures:
  551. self.apertures[current_aperture] = dict()
  552. if 'geometry' not in self.apertures[current_aperture]:
  553. self.apertures[current_aperture]['geometry'] = []
  554. self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
  555. except IndexError:
  556. log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
  557. continue
  558. # ############################################################# ##
  559. # Tool/aperture change
  560. # Example: D12*
  561. # ############################################################# ##
  562. match = self.tool_re.search(gline)
  563. if match:
  564. current_aperture = match.group(1)
  565. # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture))
  566. # If the aperture value is zero then make it something quite small but with a non-zero value
  567. # so it can be processed by FlatCAM.
  568. # But first test to see if the aperture type is "aperture macro". In that case
  569. # we should not test for "size" key as it does not exist in this case.
  570. if self.apertures[current_aperture]["type"] is not "AM":
  571. if self.apertures[current_aperture]["size"] == 0:
  572. self.apertures[current_aperture]["size"] = 1e-12
  573. # log.debug(self.apertures[current_aperture])
  574. # Take care of the current path with the previous tool
  575. if len(path) > 1:
  576. if self.apertures[last_path_aperture]["type"] == 'R':
  577. # do nothing because 'R' type moving aperture is none at once
  578. pass
  579. else:
  580. geo_dict = dict()
  581. geo_f = LineString(path)
  582. if not geo_f.is_empty:
  583. follow_buffer.append(geo_f)
  584. geo_dict['follow'] = geo_f
  585. # --- Buffered ----
  586. width = self.apertures[last_path_aperture]["size"]
  587. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  588. if not geo_s.is_empty:
  589. if self.app.defaults['gerber_simplification']:
  590. poly_buffer.append(geo_s.simplify(s_tol))
  591. else:
  592. poly_buffer.append(geo_s)
  593. if self.is_lpc is True:
  594. geo_dict['clear'] = geo_s
  595. else:
  596. geo_dict['solid'] = geo_s
  597. if last_path_aperture not in self.apertures:
  598. self.apertures[last_path_aperture] = dict()
  599. if 'geometry' not in self.apertures[last_path_aperture]:
  600. self.apertures[last_path_aperture]['geometry'] = []
  601. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  602. path = [path[-1]]
  603. continue
  604. # ############################################################# ##
  605. # G36* - Begin region
  606. # ############################################################# ##
  607. if self.regionon_re.search(gline):
  608. if len(path) > 1:
  609. # Take care of what is left in the path
  610. geo_dict = dict()
  611. geo_f = LineString(path)
  612. if not geo_f.is_empty:
  613. follow_buffer.append(geo_f)
  614. geo_dict['follow'] = geo_f
  615. # --- Buffered ----
  616. width = self.apertures[last_path_aperture]["size"]
  617. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  618. if not geo_s.is_empty:
  619. if self.app.defaults['gerber_simplification']:
  620. poly_buffer.append(geo_s.simplify(s_tol))
  621. else:
  622. poly_buffer.append(geo_s)
  623. if self.is_lpc is True:
  624. geo_dict['clear'] = geo_s
  625. else:
  626. geo_dict['solid'] = geo_s
  627. if last_path_aperture not in self.apertures:
  628. self.apertures[last_path_aperture] = dict()
  629. if 'geometry' not in self.apertures[last_path_aperture]:
  630. self.apertures[last_path_aperture]['geometry'] = []
  631. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  632. path = [path[-1]]
  633. making_region = True
  634. continue
  635. # ############################################################# ##
  636. # G37* - End region
  637. # ############################################################# ##
  638. if self.regionoff_re.search(gline):
  639. making_region = False
  640. if '0' not in self.apertures:
  641. self.apertures['0'] = {}
  642. self.apertures['0']['type'] = 'REG'
  643. self.apertures['0']['size'] = 0.0
  644. self.apertures['0']['geometry'] = list()
  645. # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
  646. # geo to the poly_buffer otherwise we loose it
  647. if current_operation_code == 2:
  648. if len(path) == 1:
  649. # this means that the geometry was prepared previously and we just need to add it
  650. geo_dict = dict()
  651. if geo_f:
  652. if not geo_f.is_empty:
  653. follow_buffer.append(geo_f)
  654. geo_dict['follow'] = geo_f
  655. if geo_s:
  656. if not geo_s.is_empty:
  657. if self.app.defaults['gerber_simplification']:
  658. poly_buffer.append(geo_s.simplify(s_tol))
  659. else:
  660. poly_buffer.append(geo_s)
  661. if self.is_lpc is True:
  662. geo_dict['clear'] = geo_s
  663. else:
  664. geo_dict['solid'] = geo_s
  665. if geo_s or geo_f:
  666. self.apertures['0']['geometry'].append(deepcopy(geo_dict))
  667. path = [[current_x, current_y]] # Start new path
  668. # Only one path defines region?
  669. # This can happen if D02 happened before G37 and
  670. # is not and error.
  671. if len(path) < 3:
  672. # print "ERROR: Path contains less than 3 points:"
  673. # path = [[current_x, current_y]]
  674. continue
  675. # For regions we may ignore an aperture that is None
  676. # --- Buffered ---
  677. geo_dict = dict()
  678. region_f = Polygon(path).exterior
  679. if not region_f.is_empty:
  680. follow_buffer.append(region_f)
  681. geo_dict['follow'] = region_f
  682. region_s = Polygon(path)
  683. if not region_s.is_valid:
  684. region_s = region_s.buffer(0, int(self.steps_per_circle / 4))
  685. if not region_s.is_empty:
  686. if self.app.defaults['gerber_simplification']:
  687. poly_buffer.append(region_s.simplify(s_tol))
  688. else:
  689. poly_buffer.append(region_s)
  690. if self.is_lpc is True:
  691. geo_dict['clear'] = region_s
  692. else:
  693. geo_dict['solid'] = region_s
  694. if not region_s.is_empty or not region_f.is_empty:
  695. self.apertures['0']['geometry'].append(deepcopy(geo_dict))
  696. path = [[current_x, current_y]] # Start new path
  697. continue
  698. # ## G01/2/3* - Interpolation mode change
  699. # Can occur along with coordinates and operation code but
  700. # sometimes by itself (handled here).
  701. # Example: G01*
  702. match = self.interp_re.search(gline)
  703. if match:
  704. current_interpolation_mode = int(match.group(1))
  705. continue
  706. # ## G01 - Linear interpolation plus flashes
  707. # Operation code (D0x) missing is deprecated... oh well I will support it.
  708. # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
  709. match = self.lin_re.search(gline)
  710. if match:
  711. # Dxx alone?
  712. # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
  713. # try:
  714. # current_operation_code = int(match.group(4))
  715. # except Exception:
  716. # pass # A line with just * will match too.
  717. # continue
  718. # NOTE: Letting it continue allows it to react to the
  719. # operation code.
  720. # Parse coordinates
  721. if match.group(2) is not None:
  722. linear_x = parse_gerber_number(match.group(2),
  723. self.int_digits, self.frac_digits, self.gerber_zeros)
  724. current_x = linear_x
  725. else:
  726. linear_x = current_x
  727. if match.group(3) is not None:
  728. linear_y = parse_gerber_number(match.group(3),
  729. self.int_digits, self.frac_digits, self.gerber_zeros)
  730. current_y = linear_y
  731. else:
  732. linear_y = current_y
  733. # Parse operation code
  734. if match.group(4) is not None:
  735. current_operation_code = int(match.group(4))
  736. # Pen down: add segment
  737. if current_operation_code == 1:
  738. # if linear_x or linear_y are None, ignore those
  739. if current_x is not None and current_y is not None:
  740. # only add the point if it's a new one otherwise skip it (harder to process)
  741. if path[-1] != [current_x, current_y]:
  742. path.append([current_x, current_y])
  743. if making_region is False:
  744. # if the aperture is rectangle then add a rectangular shape having as parameters the
  745. # coordinates of the start and end point and also the width and height
  746. # of the 'R' aperture
  747. try:
  748. if self.apertures[current_aperture]["type"] == 'R':
  749. width = self.apertures[current_aperture]['width']
  750. height = self.apertures[current_aperture]['height']
  751. minx = min(path[0][0], path[1][0]) - width / 2
  752. maxx = max(path[0][0], path[1][0]) + width / 2
  753. miny = min(path[0][1], path[1][1]) - height / 2
  754. maxy = max(path[0][1], path[1][1]) + height / 2
  755. log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
  756. geo_dict = dict()
  757. geo_f = Point([current_x, current_y])
  758. follow_buffer.append(geo_f)
  759. geo_dict['follow'] = geo_f
  760. geo_s = shply_box(minx, miny, maxx, maxy)
  761. if self.app.defaults['gerber_simplification']:
  762. poly_buffer.append(geo_s.simplify(s_tol))
  763. else:
  764. poly_buffer.append(geo_s)
  765. if self.is_lpc is True:
  766. geo_dict['clear'] = geo_s
  767. else:
  768. geo_dict['solid'] = geo_s
  769. if current_aperture not in self.apertures:
  770. self.apertures[current_aperture] = dict()
  771. if 'geometry' not in self.apertures[current_aperture]:
  772. self.apertures[current_aperture]['geometry'] = []
  773. self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
  774. except Exception as e:
  775. pass
  776. last_path_aperture = current_aperture
  777. # we do this for the case that a region is done without having defined any aperture
  778. if last_path_aperture is None:
  779. if '0' not in self.apertures:
  780. self.apertures['0'] = {}
  781. self.apertures['0']['type'] = 'REG'
  782. self.apertures['0']['size'] = 0.0
  783. self.apertures['0']['geometry'] = []
  784. last_path_aperture = '0'
  785. else:
  786. self.app.inform.emit('[WARNING] %s: %s' %
  787. (_("Coordinates missing, line ignored"), str(gline)))
  788. self.app.inform.emit('[WARNING_NOTCL] %s' %
  789. _("GERBER file might be CORRUPT. Check the file !!!"))
  790. elif current_operation_code == 2:
  791. if len(path) > 1:
  792. geo_s = None
  793. geo_dict = dict()
  794. # --- BUFFERED ---
  795. # this treats the case when we are storing geometry as paths only
  796. if making_region:
  797. # we do this for the case that a region is done without having defined any aperture
  798. if last_path_aperture is None:
  799. if '0' not in self.apertures:
  800. self.apertures['0'] = {}
  801. self.apertures['0']['type'] = 'REG'
  802. self.apertures['0']['size'] = 0.0
  803. self.apertures['0']['geometry'] = []
  804. last_path_aperture = '0'
  805. geo_f = Polygon()
  806. else:
  807. geo_f = LineString(path)
  808. try:
  809. if self.apertures[last_path_aperture]["type"] != 'R':
  810. if not geo_f.is_empty:
  811. follow_buffer.append(geo_f)
  812. geo_dict['follow'] = geo_f
  813. except Exception as e:
  814. log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
  815. if not geo_f.is_empty:
  816. follow_buffer.append(geo_f)
  817. geo_dict['follow'] = geo_f
  818. # this treats the case when we are storing geometry as solids
  819. if making_region:
  820. # we do this for the case that a region is done without having defined any aperture
  821. if last_path_aperture is None:
  822. if '0' not in self.apertures:
  823. self.apertures['0'] = {}
  824. self.apertures['0']['type'] = 'REG'
  825. self.apertures['0']['size'] = 0.0
  826. self.apertures['0']['geometry'] = []
  827. last_path_aperture = '0'
  828. try:
  829. geo_s = Polygon(path)
  830. except ValueError:
  831. log.warning("Problem %s %s" % (gline, line_num))
  832. self.app.inform.emit('[ERROR] %s: %s' %
  833. (_("Region does not have enough points. "
  834. "File will be processed but there are parser errors. "
  835. "Line number"), str(line_num)))
  836. else:
  837. if last_path_aperture is None:
  838. log.warning("No aperture defined for curent path. (%d)" % line_num)
  839. width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
  840. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  841. try:
  842. if self.apertures[last_path_aperture]["type"] != 'R':
  843. if not geo_s.is_empty:
  844. if self.app.defaults['gerber_simplification']:
  845. poly_buffer.append(geo_s.simplify(s_tol))
  846. else:
  847. poly_buffer.append(geo_s)
  848. if self.is_lpc is True:
  849. geo_dict['clear'] = geo_s
  850. else:
  851. geo_dict['solid'] = geo_s
  852. except Exception as e:
  853. log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
  854. if self.app.defaults['gerber_simplification']:
  855. poly_buffer.append(geo_s.simplify(s_tol))
  856. else:
  857. poly_buffer.append(geo_s)
  858. if self.is_lpc is True:
  859. geo_dict['clear'] = geo_s
  860. else:
  861. geo_dict['solid'] = geo_s
  862. if last_path_aperture not in self.apertures:
  863. self.apertures[last_path_aperture] = dict()
  864. if 'geometry' not in self.apertures[last_path_aperture]:
  865. self.apertures[last_path_aperture]['geometry'] = []
  866. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  867. # if linear_x or linear_y are None, ignore those
  868. if linear_x is not None and linear_y is not None:
  869. path = [[linear_x, linear_y]] # Start new path
  870. else:
  871. self.app.inform.emit('[WARNING] %s: %s' %
  872. (_("Coordinates missing, line ignored"), str(gline)))
  873. self.app.inform.emit('[WARNING_NOTCL] %s' %
  874. _("GERBER file might be CORRUPT. Check the file !!!"))
  875. # Flash
  876. # Not allowed in region mode.
  877. elif current_operation_code == 3:
  878. # Create path draw so far.
  879. if len(path) > 1:
  880. # --- Buffered ----
  881. geo_dict = dict()
  882. # this treats the case when we are storing geometry as paths
  883. geo_f = LineString(path)
  884. if not geo_f.is_empty:
  885. try:
  886. if self.apertures[last_path_aperture]["type"] != 'R':
  887. follow_buffer.append(geo_f)
  888. geo_dict['follow'] = geo_f
  889. except Exception as e:
  890. log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
  891. follow_buffer.append(geo_f)
  892. geo_dict['follow'] = geo_f
  893. # this treats the case when we are storing geometry as solids
  894. width = self.apertures[last_path_aperture]["size"]
  895. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  896. if not geo_s.is_empty:
  897. try:
  898. if self.apertures[last_path_aperture]["type"] != 'R':
  899. if self.app.defaults['gerber_simplification']:
  900. poly_buffer.append(geo_s.simplify(s_tol))
  901. else:
  902. poly_buffer.append(geo_s)
  903. if self.is_lpc is True:
  904. geo_dict['clear'] = geo_s
  905. else:
  906. geo_dict['solid'] = geo_s
  907. except Exception:
  908. if self.app.defaults['gerber_simplification']:
  909. poly_buffer.append(geo_s.simplify(s_tol))
  910. else:
  911. poly_buffer.append(geo_s)
  912. if self.is_lpc is True:
  913. geo_dict['clear'] = geo_s
  914. else:
  915. geo_dict['solid'] = geo_s
  916. if last_path_aperture not in self.apertures:
  917. self.apertures[last_path_aperture] = dict()
  918. if 'geometry' not in self.apertures[last_path_aperture]:
  919. self.apertures[last_path_aperture]['geometry'] = []
  920. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  921. # Reset path starting point
  922. path = [[linear_x, linear_y]]
  923. # --- BUFFERED ---
  924. # Draw the flash
  925. # this treats the case when we are storing geometry as paths
  926. geo_dict = dict()
  927. geo_flash = Point([linear_x, linear_y])
  928. follow_buffer.append(geo_flash)
  929. geo_dict['follow'] = geo_flash
  930. # this treats the case when we are storing geometry as solids
  931. flash = self.create_flash_geometry(
  932. Point([linear_x, linear_y]),
  933. self.apertures[current_aperture],
  934. self.steps_per_circle
  935. )
  936. if not flash.is_empty:
  937. if self.app.defaults['gerber_simplification']:
  938. poly_buffer.append(flash.simplify(s_tol))
  939. else:
  940. poly_buffer.append(flash)
  941. if self.is_lpc is True:
  942. geo_dict['clear'] = flash
  943. else:
  944. geo_dict['solid'] = flash
  945. if current_aperture not in self.apertures:
  946. self.apertures[current_aperture] = dict()
  947. if 'geometry' not in self.apertures[current_aperture]:
  948. self.apertures[current_aperture]['geometry'] = []
  949. self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
  950. # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
  951. # are used in case that circular interpolation is encountered within the Gerber file
  952. current_x = linear_x
  953. current_y = linear_y
  954. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  955. continue
  956. # ## G74/75* - Single or multiple quadrant arcs
  957. match = self.quad_re.search(gline)
  958. if match:
  959. if match.group(1) == '4':
  960. quadrant_mode = 'SINGLE'
  961. else:
  962. quadrant_mode = 'MULTI'
  963. continue
  964. # ## G02/3 - Circular interpolation
  965. # 2-clockwise, 3-counterclockwise
  966. # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
  967. match = self.circ_re.search(gline)
  968. if match:
  969. arcdir = [None, None, "cw", "ccw"]
  970. mode, circular_x, circular_y, i, j, d = match.groups()
  971. try:
  972. circular_x = parse_gerber_number(circular_x,
  973. self.int_digits, self.frac_digits, self.gerber_zeros)
  974. except Exception as e:
  975. circular_x = current_x
  976. try:
  977. circular_y = parse_gerber_number(circular_y,
  978. self.int_digits, self.frac_digits, self.gerber_zeros)
  979. except Exception as e:
  980. circular_y = current_y
  981. # According to Gerber specification i and j are not modal, which means that when i or j are missing,
  982. # they are to be interpreted as being zero
  983. try:
  984. i = parse_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
  985. except Exception as e:
  986. i = 0
  987. try:
  988. j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
  989. except Exception as e:
  990. j = 0
  991. if quadrant_mode is None:
  992. log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
  993. log.error(gline)
  994. continue
  995. if mode is None and current_interpolation_mode not in [2, 3]:
  996. log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  997. log.error(gline)
  998. continue
  999. elif mode is not None:
  1000. current_interpolation_mode = int(mode)
  1001. # Set operation code if provided
  1002. if d is not None:
  1003. current_operation_code = int(d)
  1004. # Nothing created! Pen Up.
  1005. if current_operation_code == 2:
  1006. log.warning("Arc with D2. (%d)" % line_num)
  1007. if len(path) > 1:
  1008. geo_dict = dict()
  1009. if last_path_aperture is None:
  1010. log.warning("No aperture defined for curent path. (%d)" % line_num)
  1011. # --- BUFFERED ---
  1012. width = self.apertures[last_path_aperture]["size"]
  1013. # this treats the case when we are storing geometry as paths
  1014. geo_f = LineString(path)
  1015. if not geo_f.is_empty:
  1016. follow_buffer.append(geo_f)
  1017. geo_dict['follow'] = geo_f
  1018. # this treats the case when we are storing geometry as solids
  1019. buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
  1020. if not buffered.is_empty:
  1021. if self.app.defaults['gerber_simplification']:
  1022. poly_buffer.append(buffered.simplify(s_tol))
  1023. else:
  1024. poly_buffer.append(buffered)
  1025. if self.is_lpc is True:
  1026. geo_dict['clear'] = buffered
  1027. else:
  1028. geo_dict['solid'] = buffered
  1029. if last_path_aperture not in self.apertures:
  1030. self.apertures[last_path_aperture] = dict()
  1031. if 'geometry' not in self.apertures[last_path_aperture]:
  1032. self.apertures[last_path_aperture]['geometry'] = []
  1033. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  1034. current_x = circular_x
  1035. current_y = circular_y
  1036. path = [[current_x, current_y]] # Start new path
  1037. continue
  1038. # Flash should not happen here
  1039. if current_operation_code == 3:
  1040. log.error("Trying to flash within arc. (%d)" % line_num)
  1041. continue
  1042. if quadrant_mode == 'MULTI':
  1043. center = [i + current_x, j + current_y]
  1044. radius = np.sqrt(i ** 2 + j ** 2)
  1045. start = np.arctan2(-j, -i) # Start angle
  1046. # Numerical errors might prevent start == stop therefore
  1047. # we check ahead of time. This should result in a
  1048. # 360 degree arc.
  1049. if current_x == circular_x and current_y == circular_y:
  1050. stop = start
  1051. else:
  1052. stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  1053. this_arc = arc(center, radius, start, stop,
  1054. arcdir[current_interpolation_mode],
  1055. self.steps_per_circle)
  1056. # The last point in the computed arc can have
  1057. # numerical errors. The exact final point is the
  1058. # specified (x, y). Replace.
  1059. this_arc[-1] = (circular_x, circular_y)
  1060. # Last point in path is current point
  1061. # current_x = this_arc[-1][0]
  1062. # current_y = this_arc[-1][1]
  1063. current_x, current_y = circular_x, circular_y
  1064. # Append
  1065. path += this_arc
  1066. last_path_aperture = current_aperture
  1067. continue
  1068. if quadrant_mode == 'SINGLE':
  1069. center_candidates = [
  1070. [i + current_x, j + current_y],
  1071. [-i + current_x, j + current_y],
  1072. [i + current_x, -j + current_y],
  1073. [-i + current_x, -j + current_y]
  1074. ]
  1075. valid = False
  1076. log.debug("I: %f J: %f" % (i, j))
  1077. for center in center_candidates:
  1078. radius = np.sqrt(i ** 2 + j ** 2)
  1079. # Make sure radius to start is the same as radius to end.
  1080. radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
  1081. if radius2 < radius * 0.95 or radius2 > radius * 1.05:
  1082. continue # Not a valid center.
  1083. # Correct i and j and continue as with multi-quadrant.
  1084. i = center[0] - current_x
  1085. j = center[1] - current_y
  1086. start = np.arctan2(-j, -i) # Start angle
  1087. stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  1088. angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  1089. log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  1090. (current_x, current_y, center[0], center[1], circular_x, circular_y))
  1091. log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  1092. (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
  1093. angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
  1094. if angle <= (np.pi + 1e-6) / 2:
  1095. log.debug("########## ACCEPTING ARC ############")
  1096. this_arc = arc(center, radius, start, stop,
  1097. arcdir[current_interpolation_mode],
  1098. self.steps_per_circle)
  1099. # Replace with exact values
  1100. this_arc[-1] = (circular_x, circular_y)
  1101. # current_x = this_arc[-1][0]
  1102. # current_y = this_arc[-1][1]
  1103. current_x, current_y = circular_x, circular_y
  1104. path += this_arc
  1105. last_path_aperture = current_aperture
  1106. valid = True
  1107. break
  1108. if valid:
  1109. continue
  1110. else:
  1111. log.warning("Invalid arc in line %d." % line_num)
  1112. # ## EOF
  1113. match = self.eof_re.search(gline)
  1114. if match:
  1115. continue
  1116. # ## Line did not match any pattern. Warn user.
  1117. log.warning("Line ignored (%d): %s" % (line_num, gline))
  1118. if len(path) > 1:
  1119. # In case that G01 (moving) aperture is rectangular, there is no need to still create
  1120. # another geo since we already created a shapely box using the start and end coordinates found in
  1121. # path variable. We do it only for other apertures than 'R' type
  1122. if self.apertures[last_path_aperture]["type"] == 'R':
  1123. pass
  1124. else:
  1125. # EOF, create shapely LineString if something still in path
  1126. # ## --- Buffered ---
  1127. geo_dict = dict()
  1128. # this treats the case when we are storing geometry as paths
  1129. geo_f = LineString(path)
  1130. if not geo_f.is_empty:
  1131. follow_buffer.append(geo_f)
  1132. geo_dict['follow'] = geo_f
  1133. # this treats the case when we are storing geometry as solids
  1134. width = self.apertures[last_path_aperture]["size"]
  1135. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  1136. if not geo_s.is_empty:
  1137. if self.app.defaults['gerber_simplification']:
  1138. poly_buffer.append(geo_s.simplify(s_tol))
  1139. else:
  1140. poly_buffer.append(geo_s)
  1141. if self.is_lpc is True:
  1142. geo_dict['clear'] = geo_s
  1143. else:
  1144. geo_dict['solid'] = geo_s
  1145. if last_path_aperture not in self.apertures:
  1146. self.apertures[last_path_aperture] = dict()
  1147. if 'geometry' not in self.apertures[last_path_aperture]:
  1148. self.apertures[last_path_aperture]['geometry'] = []
  1149. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  1150. # --- Apply buffer ---
  1151. # this treats the case when we are storing geometry as paths
  1152. self.follow_geometry = follow_buffer
  1153. # this treats the case when we are storing geometry as solids
  1154. if len(poly_buffer) == 0 and len(self.solid_geometry) == 0:
  1155. log.error("Object is not Gerber file or empty. Aborting Object creation.")
  1156. return 'fail'
  1157. log.warning("Joining %d polygons." % len(poly_buffer))
  1158. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
  1159. if self.use_buffer_for_union:
  1160. log.debug("Union by buffer...")
  1161. new_poly = MultiPolygon(poly_buffer)
  1162. if self.app.defaults["gerber_buffering"] == 'full':
  1163. new_poly = new_poly.buffer(0.00000001)
  1164. new_poly = new_poly.buffer(-0.00000001)
  1165. log.warning("Union(buffer) done.")
  1166. else:
  1167. log.debug("Union by union()...")
  1168. new_poly = cascaded_union(poly_buffer)
  1169. new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
  1170. log.warning("Union done.")
  1171. if current_polarity == 'D':
  1172. self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
  1173. if new_poly.is_valid:
  1174. self.solid_geometry = self.solid_geometry.union(new_poly)
  1175. else:
  1176. # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
  1177. # loaded. Instead of applying a union I add to a list of polygons.
  1178. final_poly = []
  1179. try:
  1180. for poly in new_poly:
  1181. final_poly.append(poly)
  1182. except TypeError:
  1183. final_poly.append(new_poly)
  1184. try:
  1185. for poly in self.solid_geometry:
  1186. final_poly.append(poly)
  1187. except TypeError:
  1188. final_poly.append(self.solid_geometry)
  1189. self.solid_geometry = final_poly
  1190. # try:
  1191. # self.solid_geometry = self.solid_geometry.union(new_poly)
  1192. # except Exception as e:
  1193. # # in case in the new_poly are some self intersections try to avoid making union with them
  1194. # for poly in new_poly:
  1195. # try:
  1196. # self.solid_geometry = self.solid_geometry.union(poly)
  1197. # except Exception:
  1198. # pass
  1199. else:
  1200. self.solid_geometry = self.solid_geometry.difference(new_poly)
  1201. # init this for the following operations
  1202. self.conversion_done = False
  1203. except Exception as err:
  1204. ex_type, ex, tb = sys.exc_info()
  1205. traceback.print_tb(tb)
  1206. # print traceback.format_exc()
  1207. log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
  1208. loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
  1209. self.app.inform.emit('[ERROR] %s\n%s:' %
  1210. (_("Gerber Parser ERROR"), loc))
  1211. @staticmethod
  1212. def create_flash_geometry(location, aperture, steps_per_circle=None):
  1213. # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
  1214. if type(location) == list:
  1215. location = Point(location)
  1216. if aperture['type'] == 'C': # Circles
  1217. return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4))
  1218. if aperture['type'] == 'R': # Rectangles
  1219. loc = location.coords[0]
  1220. width = aperture['width']
  1221. height = aperture['height']
  1222. minx = loc[0] - width / 2
  1223. maxx = loc[0] + width / 2
  1224. miny = loc[1] - height / 2
  1225. maxy = loc[1] + height / 2
  1226. return shply_box(minx, miny, maxx, maxy)
  1227. if aperture['type'] == 'O': # Obround
  1228. loc = location.coords[0]
  1229. width = aperture['width']
  1230. height = aperture['height']
  1231. if width > height:
  1232. p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
  1233. p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
  1234. c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4))
  1235. c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4))
  1236. else:
  1237. p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
  1238. p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
  1239. c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
  1240. c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
  1241. return cascaded_union([c1, c2]).convex_hull
  1242. if aperture['type'] == 'P': # Regular polygon
  1243. loc = location.coords[0]
  1244. diam = aperture['diam']
  1245. n_vertices = aperture['nVertices']
  1246. points = []
  1247. for i in range(0, n_vertices):
  1248. x = loc[0] + 0.5 * diam * (np.cos(2 * np.pi * i / n_vertices))
  1249. y = loc[1] + 0.5 * diam * (np.sin(2 * np.pi * i / n_vertices))
  1250. points.append((x, y))
  1251. ply = Polygon(points)
  1252. if 'rotation' in aperture:
  1253. ply = affinity.rotate(ply, aperture['rotation'])
  1254. return ply
  1255. if aperture['type'] == 'AM': # Aperture Macro
  1256. loc = location.coords[0]
  1257. flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
  1258. if flash_geo.is_empty:
  1259. log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
  1260. return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
  1261. log.warning("Unknown aperture type: %s" % aperture['type'])
  1262. return None
  1263. def create_geometry(self):
  1264. """
  1265. Geometry from a Gerber file is made up entirely of polygons.
  1266. Every stroke (linear or circular) has an aperture which gives
  1267. it thickness. Additionally, aperture strokes have non-zero area,
  1268. and regions naturally do as well.
  1269. :rtype : None
  1270. :return: None
  1271. """
  1272. pass
  1273. # self.buffer_paths()
  1274. #
  1275. # self.fix_regions()
  1276. #
  1277. # self.do_flashes()
  1278. #
  1279. # self.solid_geometry = cascaded_union(self.buffered_paths +
  1280. # [poly['polygon'] for poly in self.regions] +
  1281. # self.flash_geometry)
  1282. def get_bounding_box(self, margin=0.0, rounded=False):
  1283. """
  1284. Creates and returns a rectangular polygon bounding at a distance of
  1285. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  1286. can optionally have rounded corners of radius equal to margin.
  1287. :param margin: Distance to enlarge the rectangular bounding
  1288. box in both positive and negative, x and y axes.
  1289. :type margin: float
  1290. :param rounded: Wether or not to have rounded corners.
  1291. :type rounded: bool
  1292. :return: The bounding box.
  1293. :rtype: Shapely.Polygon
  1294. """
  1295. bbox = self.solid_geometry.envelope.buffer(margin)
  1296. if not rounded:
  1297. bbox = bbox.envelope
  1298. return bbox
  1299. def bounds(self):
  1300. """
  1301. Returns coordinates of rectangular bounds
  1302. of Gerber geometry: (xmin, ymin, xmax, ymax).
  1303. """
  1304. # fixed issue of getting bounds only for one level lists of objects
  1305. # now it can get bounds for nested lists of objects
  1306. log.debug("parseGerber.Gerber.bounds()")
  1307. if self.solid_geometry is None:
  1308. log.debug("solid_geometry is None")
  1309. return 0, 0, 0, 0
  1310. def bounds_rec(obj):
  1311. if type(obj) is list and type(obj) is not MultiPolygon:
  1312. minx = np.Inf
  1313. miny = np.Inf
  1314. maxx = -np.Inf
  1315. maxy = -np.Inf
  1316. for k in obj:
  1317. if type(k) is dict:
  1318. for key in k:
  1319. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  1320. minx = min(minx, minx_)
  1321. miny = min(miny, miny_)
  1322. maxx = max(maxx, maxx_)
  1323. maxy = max(maxy, maxy_)
  1324. else:
  1325. if not k.is_empty:
  1326. try:
  1327. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  1328. except Exception as e:
  1329. log.debug("camlib.Gerber.bounds() --> %s" % str(e))
  1330. return
  1331. minx = min(minx, minx_)
  1332. miny = min(miny, miny_)
  1333. maxx = max(maxx, maxx_)
  1334. maxy = max(maxy, maxy_)
  1335. return minx, miny, maxx, maxy
  1336. else:
  1337. # it's a Shapely object, return it's bounds
  1338. return obj.bounds
  1339. bounds_coords = bounds_rec(self.solid_geometry)
  1340. return bounds_coords
  1341. def convert_units(self, obj_units):
  1342. """
  1343. Converts the units of the object to ``units`` by scaling all
  1344. the geometry appropriately. This call ``scale()``. Don't call
  1345. it again in descendants.
  1346. :param units: "IN" or "MM"
  1347. :type units: str
  1348. :return: Scaling factor resulting from unit change.
  1349. :rtype: float
  1350. """
  1351. if obj_units.upper() == self.units.upper():
  1352. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  1353. return 1.0
  1354. if obj_units.upper() == "MM":
  1355. factor = 25.4
  1356. log.debug("parseGerber.Gerber.convert_units() --> Factor: 25.4")
  1357. elif obj_units.upper() == "IN":
  1358. factor = 1 / 25.4
  1359. log.debug("parseGerber.Gerber.convert_units() --> Factor: %s" % str(1 / 25.4))
  1360. else:
  1361. log.error("Unsupported units: %s" % str(obj_units))
  1362. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  1363. return 1.0
  1364. self.units = obj_units
  1365. self.file_units_factor = factor
  1366. self.scale(factor, factor)
  1367. return factor
  1368. def import_svg(self, filename, object_type='gerber', flip=True, units='MM'):
  1369. """
  1370. Imports shapes from an SVG file into the object's geometry.
  1371. :param filename: Path to the SVG file.
  1372. :type filename: str
  1373. :param object_type: parameter passed further along
  1374. :param flip: Flip the vertically.
  1375. :type flip: bool
  1376. :param units: FlatCAM units
  1377. :return: None
  1378. """
  1379. log.debug("flatcamParsers.ParseGerber.Gerber.import_svg()")
  1380. # Parse into list of shapely objects
  1381. svg_tree = ET.parse(filename)
  1382. svg_root = svg_tree.getroot()
  1383. # Change origin to bottom left
  1384. # h = float(svg_root.get('height'))
  1385. # w = float(svg_root.get('width'))
  1386. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  1387. geos = getsvggeo(svg_root, 'gerber')
  1388. if flip:
  1389. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  1390. # Add to object
  1391. if self.solid_geometry is None:
  1392. self.solid_geometry = list()
  1393. # if type(self.solid_geometry) == list:
  1394. # if type(geos) == list:
  1395. # self.solid_geometry += geos
  1396. # else:
  1397. # self.solid_geometry.append(geos)
  1398. # else: # It's shapely geometry
  1399. # self.solid_geometry = [self.solid_geometry, geos]
  1400. if type(geos) == list:
  1401. # HACK for importing QRCODE exported by FlatCAM
  1402. if len(geos) == 1:
  1403. geo_qrcode = list()
  1404. geo_qrcode.append(Polygon(geos[0].exterior))
  1405. for i_el in geos[0].interiors:
  1406. geo_qrcode.append(Polygon(i_el).buffer(0))
  1407. for poly in geo_qrcode:
  1408. geos.append(poly)
  1409. if type(self.solid_geometry) == list:
  1410. self.solid_geometry += geos
  1411. else:
  1412. geos.append(self.solid_geometry)
  1413. self.solid_geometry = geos
  1414. else:
  1415. if type(self.solid_geometry) == list:
  1416. self.solid_geometry.append(geos)
  1417. else:
  1418. self.solid_geometry = [self.solid_geometry, geos]
  1419. # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
  1420. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  1421. try:
  1422. __ = iter(self.solid_geometry)
  1423. except TypeError:
  1424. self.solid_geometry = [self.solid_geometry]
  1425. if '0' not in self.apertures:
  1426. self.apertures['0'] = dict()
  1427. self.apertures['0']['type'] = 'REG'
  1428. self.apertures['0']['size'] = 0.0
  1429. self.apertures['0']['geometry'] = list()
  1430. for pol in self.solid_geometry:
  1431. new_el = dict()
  1432. new_el['solid'] = pol
  1433. new_el['follow'] = pol.exterior
  1434. self.apertures['0']['geometry'].append(deepcopy(new_el))
  1435. def scale(self, xfactor, yfactor=None, point=None):
  1436. """
  1437. Scales the objects' geometry on the XY plane by a given factor.
  1438. These are:
  1439. * ``buffered_paths``
  1440. * ``flash_geometry``
  1441. * ``solid_geometry``
  1442. * ``regions``
  1443. NOTE:
  1444. Does not modify the data used to create these elements. If these
  1445. are recreated, the scaling will be lost. This behavior was modified
  1446. because of the complexity reached in this class.
  1447. :param xfactor: Number by which to scale on X axis.
  1448. :type xfactor: float
  1449. :param yfactor: Number by which to scale on Y axis.
  1450. :type yfactor: float
  1451. :param point: reference point for scaling operation
  1452. :rtype : None
  1453. """
  1454. log.debug("parseGerber.Gerber.scale()")
  1455. try:
  1456. xfactor = float(xfactor)
  1457. except Exception:
  1458. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1459. _("Scale factor has to be a number: integer or float."))
  1460. return
  1461. if yfactor is None:
  1462. yfactor = xfactor
  1463. else:
  1464. try:
  1465. yfactor = float(yfactor)
  1466. except Exception:
  1467. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1468. _("Scale factor has to be a number: integer or float."))
  1469. return
  1470. if point is None:
  1471. px = 0
  1472. py = 0
  1473. else:
  1474. px, py = point
  1475. # variables to display the percentage of work done
  1476. self.geo_len = 0
  1477. try:
  1478. for __ in self.solid_geometry:
  1479. self.geo_len += 1
  1480. except TypeError:
  1481. self.geo_len = 1
  1482. self.old_disp_number = 0
  1483. self.el_count = 0
  1484. def scale_geom(obj):
  1485. if type(obj) is list:
  1486. new_obj = []
  1487. for g in obj:
  1488. new_obj.append(scale_geom(g))
  1489. return new_obj
  1490. else:
  1491. try:
  1492. self.el_count += 1
  1493. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  1494. if self.old_disp_number < disp_number <= 100:
  1495. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1496. self.old_disp_number = disp_number
  1497. return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
  1498. except AttributeError:
  1499. return obj
  1500. self.solid_geometry = scale_geom(self.solid_geometry)
  1501. self.follow_geometry = scale_geom(self.follow_geometry)
  1502. # we need to scale the geometry stored in the Gerber apertures, too
  1503. try:
  1504. for apid in self.apertures:
  1505. new_geometry = list()
  1506. if 'geometry' in self.apertures[apid]:
  1507. for geo_el in self.apertures[apid]['geometry']:
  1508. new_geo_el = dict()
  1509. if 'solid' in geo_el:
  1510. new_geo_el['solid'] = scale_geom(geo_el['solid'])
  1511. if 'follow' in geo_el:
  1512. new_geo_el['follow'] = scale_geom(geo_el['follow'])
  1513. if 'clear' in geo_el:
  1514. new_geo_el['clear'] = scale_geom(geo_el['clear'])
  1515. new_geometry.append(new_geo_el)
  1516. self.apertures[apid]['geometry'] = deepcopy(new_geometry)
  1517. try:
  1518. if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
  1519. self.apertures[apid]['width'] *= xfactor
  1520. self.apertures[apid]['height'] *= xfactor
  1521. elif str(self.apertures[apid]['type']) == 'P':
  1522. self.apertures[apid]['diam'] *= xfactor
  1523. self.apertures[apid]['nVertices'] *= xfactor
  1524. except KeyError:
  1525. pass
  1526. try:
  1527. if self.apertures[apid]['size'] is not None:
  1528. self.apertures[apid]['size'] = float(self.apertures[apid]['size'] * xfactor)
  1529. except KeyError:
  1530. pass
  1531. except Exception as e:
  1532. log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
  1533. return 'fail'
  1534. self.app.inform.emit('[success] %s' %
  1535. _("Gerber Scale done."))
  1536. self.app.proc_container.new_text = ''
  1537. # ## solid_geometry ???
  1538. # It's a cascaded union of objects.
  1539. # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  1540. # factor, origin=(0, 0))
  1541. # # Now buffered_paths, flash_geometry and solid_geometry
  1542. # self.create_geometry()
  1543. def offset(self, vect):
  1544. """
  1545. Offsets the objects' geometry on the XY plane by a given vector.
  1546. These are:
  1547. * ``buffered_paths``
  1548. * ``flash_geometry``
  1549. * ``solid_geometry``
  1550. * ``regions``
  1551. NOTE:
  1552. Does not modify the data used to create these elements. If these
  1553. are recreated, the scaling will be lost. This behavior was modified
  1554. because of the complexity reached in this class.
  1555. :param vect: (x, y) offset vector.
  1556. :type vect: tuple
  1557. :return: None
  1558. """
  1559. log.debug("parseGerber.Gerber.offset()")
  1560. try:
  1561. dx, dy = vect
  1562. except TypeError:
  1563. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1564. _("An (x,y) pair of values are needed. "
  1565. "Probable you entered only one value in the Offset field."))
  1566. return
  1567. # variables to display the percentage of work done
  1568. self.geo_len = 0
  1569. try:
  1570. for __ in self.solid_geometry:
  1571. self.geo_len += 1
  1572. except TypeError:
  1573. self.geo_len = 1
  1574. self.old_disp_number = 0
  1575. self.el_count = 0
  1576. def offset_geom(obj):
  1577. if type(obj) is list:
  1578. new_obj = []
  1579. for g in obj:
  1580. new_obj.append(offset_geom(g))
  1581. return new_obj
  1582. else:
  1583. try:
  1584. self.el_count += 1
  1585. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  1586. if self.old_disp_number < disp_number <= 100:
  1587. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1588. self.old_disp_number = disp_number
  1589. return affinity.translate(obj, xoff=dx, yoff=dy)
  1590. except AttributeError:
  1591. return obj
  1592. # ## Solid geometry
  1593. self.solid_geometry = offset_geom(self.solid_geometry)
  1594. self.follow_geometry = offset_geom(self.follow_geometry)
  1595. # we need to offset the geometry stored in the Gerber apertures, too
  1596. try:
  1597. for apid in self.apertures:
  1598. if 'geometry' in self.apertures[apid]:
  1599. for geo_el in self.apertures[apid]['geometry']:
  1600. if 'solid' in geo_el:
  1601. geo_el['solid'] = offset_geom(geo_el['solid'])
  1602. if 'follow' in geo_el:
  1603. geo_el['follow'] = offset_geom(geo_el['follow'])
  1604. if 'clear' in geo_el:
  1605. geo_el['clear'] = offset_geom(geo_el['clear'])
  1606. except Exception as e:
  1607. log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
  1608. return 'fail'
  1609. self.app.inform.emit('[success] %s' %
  1610. _("Gerber Offset done."))
  1611. self.app.proc_container.new_text = ''
  1612. def mirror(self, axis, point):
  1613. """
  1614. Mirrors the object around a specified axis passing through
  1615. the given point. What is affected:
  1616. * ``buffered_paths``
  1617. * ``flash_geometry``
  1618. * ``solid_geometry``
  1619. * ``regions``
  1620. NOTE:
  1621. Does not modify the data used to create these elements. If these
  1622. are recreated, the scaling will be lost. This behavior was modified
  1623. because of the complexity reached in this class.
  1624. :param axis: "X" or "Y" indicates around which axis to mirror.
  1625. :type axis: str
  1626. :param point: [x, y] point belonging to the mirror axis.
  1627. :type point: list
  1628. :return: None
  1629. """
  1630. log.debug("parseGerber.Gerber.mirror()")
  1631. px, py = point
  1632. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1633. # variables to display the percentage of work done
  1634. self.geo_len = 0
  1635. try:
  1636. for __ in self.solid_geometry:
  1637. self.geo_len += 1
  1638. except TypeError:
  1639. self.geo_len = 1
  1640. self.old_disp_number = 0
  1641. self.el_count = 0
  1642. def mirror_geom(obj):
  1643. if type(obj) is list:
  1644. new_obj = []
  1645. for g in obj:
  1646. new_obj.append(mirror_geom(g))
  1647. return new_obj
  1648. else:
  1649. try:
  1650. self.el_count += 1
  1651. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  1652. if self.old_disp_number < disp_number <= 100:
  1653. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1654. self.old_disp_number = disp_number
  1655. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  1656. except AttributeError:
  1657. return obj
  1658. self.solid_geometry = mirror_geom(self.solid_geometry)
  1659. self.follow_geometry = mirror_geom(self.follow_geometry)
  1660. # we need to mirror the geometry stored in the Gerber apertures, too
  1661. try:
  1662. for apid in self.apertures:
  1663. if 'geometry' in self.apertures[apid]:
  1664. for geo_el in self.apertures[apid]['geometry']:
  1665. if 'solid' in geo_el:
  1666. geo_el['solid'] = mirror_geom(geo_el['solid'])
  1667. if 'follow' in geo_el:
  1668. geo_el['follow'] = mirror_geom(geo_el['follow'])
  1669. if 'clear' in geo_el:
  1670. geo_el['clear'] = mirror_geom(geo_el['clear'])
  1671. except Exception as e:
  1672. log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
  1673. return 'fail'
  1674. self.app.inform.emit('[success] %s' %
  1675. _("Gerber Mirror done."))
  1676. self.app.proc_container.new_text = ''
  1677. def skew(self, angle_x, angle_y, point):
  1678. """
  1679. Shear/Skew the geometries of an object by angles along x and y dimensions.
  1680. Parameters
  1681. ----------
  1682. angle_x, angle_y : float, float
  1683. The shear angle(s) for the x and y axes respectively. These can be
  1684. specified in either degrees (default) or radians by setting
  1685. use_radians=True.
  1686. See shapely manual for more information:
  1687. http://toblerity.org/shapely/manual.html#affine-transformations
  1688. :param angle_x: the angle on X axis for skewing
  1689. :param angle_y: the angle on Y axis for skewing
  1690. :param point: reference point for skewing operation
  1691. :return None
  1692. """
  1693. log.debug("parseGerber.Gerber.skew()")
  1694. px, py = point
  1695. # variables to display the percentage of work done
  1696. self.geo_len = 0
  1697. try:
  1698. for __ in self.solid_geometry:
  1699. self.geo_len += 1
  1700. except TypeError:
  1701. self.geo_len = 1
  1702. self.old_disp_number = 0
  1703. self.el_count = 0
  1704. def skew_geom(obj):
  1705. if type(obj) is list:
  1706. new_obj = []
  1707. for g in obj:
  1708. new_obj.append(skew_geom(g))
  1709. return new_obj
  1710. else:
  1711. try:
  1712. self.el_count += 1
  1713. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1714. if self.old_disp_number < disp_number <= 100:
  1715. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1716. self.old_disp_number = disp_number
  1717. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  1718. except AttributeError:
  1719. return obj
  1720. self.solid_geometry = skew_geom(self.solid_geometry)
  1721. self.follow_geometry = skew_geom(self.follow_geometry)
  1722. # we need to skew the geometry stored in the Gerber apertures, too
  1723. try:
  1724. for apid in self.apertures:
  1725. if 'geometry' in self.apertures[apid]:
  1726. for geo_el in self.apertures[apid]['geometry']:
  1727. if 'solid' in geo_el:
  1728. geo_el['solid'] = skew_geom(geo_el['solid'])
  1729. if 'follow' in geo_el:
  1730. geo_el['follow'] = skew_geom(geo_el['follow'])
  1731. if 'clear' in geo_el:
  1732. geo_el['clear'] = skew_geom(geo_el['clear'])
  1733. except Exception as e:
  1734. log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
  1735. return 'fail'
  1736. self.app.inform.emit('[success] %s' % _("Gerber Skew done."))
  1737. self.app.proc_container.new_text = ''
  1738. def rotate(self, angle, point):
  1739. """
  1740. Rotate an object by a given angle around given coords (point)
  1741. :param angle:
  1742. :param point:
  1743. :return:
  1744. """
  1745. log.debug("parseGerber.Gerber.rotate()")
  1746. px, py = point
  1747. # variables to display the percentage of work done
  1748. self.geo_len = 0
  1749. try:
  1750. for __ in self.solid_geometry:
  1751. self.geo_len += 1
  1752. except TypeError:
  1753. self.geo_len = 1
  1754. self.old_disp_number = 0
  1755. self.el_count = 0
  1756. def rotate_geom(obj):
  1757. if type(obj) is list:
  1758. new_obj = []
  1759. for g in obj:
  1760. new_obj.append(rotate_geom(g))
  1761. return new_obj
  1762. else:
  1763. try:
  1764. self.el_count += 1
  1765. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1766. if self.old_disp_number < disp_number <= 100:
  1767. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1768. self.old_disp_number = disp_number
  1769. return affinity.rotate(obj, angle, origin=(px, py))
  1770. except AttributeError:
  1771. return obj
  1772. self.solid_geometry = rotate_geom(self.solid_geometry)
  1773. self.follow_geometry = rotate_geom(self.follow_geometry)
  1774. # we need to rotate the geometry stored in the Gerber apertures, too
  1775. try:
  1776. for apid in self.apertures:
  1777. if 'geometry' in self.apertures[apid]:
  1778. for geo_el in self.apertures[apid]['geometry']:
  1779. if 'solid' in geo_el:
  1780. geo_el['solid'] = rotate_geom(geo_el['solid'])
  1781. if 'follow' in geo_el:
  1782. geo_el['follow'] = rotate_geom(geo_el['follow'])
  1783. if 'clear' in geo_el:
  1784. geo_el['clear'] = rotate_geom(geo_el['clear'])
  1785. except Exception as e:
  1786. log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
  1787. return 'fail'
  1788. self.app.inform.emit('[success] %s' %
  1789. _("Gerber Rotate done."))
  1790. self.app.proc_container.new_text = ''
  1791. def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
  1792. """
  1793. Parse a single number of Gerber coordinates.
  1794. :param strnumber: String containing a number in decimal digits
  1795. from a coordinate data block, possibly with a leading sign.
  1796. :type strnumber: str
  1797. :param int_digits: Number of digits used for the integer
  1798. part of the number
  1799. :type frac_digits: int
  1800. :param frac_digits: Number of digits used for the fractional
  1801. part of the number
  1802. :type frac_digits: int
  1803. :param zeros: If 'L', leading zeros are removed and trailing zeros are kept. Same situation for 'D' when
  1804. no zero suppression is done. If 'T', is in reverse.
  1805. :type zeros: str
  1806. :return: The number in floating point.
  1807. :rtype: float
  1808. """
  1809. ret_val = None
  1810. if zeros == 'L' or zeros == 'D':
  1811. ret_val = int(strnumber) * (10 ** (-frac_digits))
  1812. if zeros == 'T':
  1813. int_val = int(strnumber)
  1814. ret_val = (int_val * (10 ** ((int_digits + frac_digits) - len(strnumber)))) * (10 ** (-frac_digits))
  1815. return ret_val