ParseGerber.py 109 KB

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