ParseGerber.py 92 KB

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