ParseGerber.py 100 KB

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