ParseGerber.py 92 KB

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