ParseGerber.py 112 KB

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