ParseGerber.py 88 KB

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