ParseGerber.py 107 KB

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