ParseHPGL2.py 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. # ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrina Stanciu (c) #
  5. # Date: 12/11/2019 #
  6. # MIT Licence #
  7. # ############################################################
  8. from camlib import Geometry, arc, arc_angle
  9. import FlatCAMApp
  10. import numpy as np
  11. import re
  12. import logging
  13. import traceback
  14. from copy import deepcopy
  15. import sys
  16. from shapely.ops import cascaded_union, unary_union
  17. from shapely.geometry import Polygon, MultiPolygon, LineString, Point
  18. import shapely.affinity as affinity
  19. from shapely.geometry import box as shply_box
  20. import FlatCAMTranslation as fcTranslate
  21. import gettext
  22. import builtins
  23. if '_' not in builtins.__dict__:
  24. _ = gettext.gettext
  25. log = logging.getLogger('base')
  26. class HPGL2(Geometry):
  27. """
  28. HPGL2 parsing.
  29. """
  30. defaults = {
  31. "steps_per_circle": 64,
  32. "use_buffer_for_union": True
  33. }
  34. def __init__(self, steps_per_circle=None):
  35. """
  36. The constructor takes no parameters.
  37. :return: Geometry object
  38. :rtype: Geometry
  39. """
  40. # How to approximate a circle with lines.
  41. self.steps_per_circle = steps_per_circle if steps_per_circle is not None else \
  42. int(self.app.defaults["geometry_circle_steps"])
  43. self.decimals = self.app.decimals
  44. # Initialize parent
  45. Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
  46. # Number format
  47. self.coord_mm_factor = 0.040
  48. # store the file units here:
  49. self.units = self.app.defaults['gerber_def_units']
  50. # will store the geometry's as solids
  51. self.solid_geometry = None
  52. # will store the geometry's as paths
  53. self.follow_geometry = []
  54. self.source_file = ''
  55. # Attributes to be included in serialization
  56. # Always append to it because it carries contents
  57. # from Geometry.
  58. self.ser_attrs += ['solid_geometry', 'follow_geometry', 'source_file']
  59. # ### Parser patterns ## ##
  60. # comment
  61. self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
  62. # absolute move to x, y
  63. self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
  64. # relative move to x, y
  65. self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
  66. # pen position
  67. self.pen_re = re.compile(r"^(P[U|D]);?$")
  68. # Initialize
  69. self.mode_re = re.compile(r'^(IN);?$')
  70. # select pen
  71. self.sp_re = re.compile(r'SP(\d);?$')
  72. self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
  73. self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
  74. # G01... - Linear interpolation plus flashes with coordinates
  75. # Operation code (D0x) missing is deprecated... oh well I will support it.
  76. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  77. # G02/3... - Circular interpolation with coordinates
  78. # 2-clockwise, 3-counterclockwise
  79. # Operation code (D0x) missing is deprecated... oh well I will support it.
  80. # Optional start with G02 or G03, optional end with D01 or D02 with
  81. # optional coordinates but at least one in any order.
  82. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' +
  83. '?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  84. # G01/2/3 Occurring without coordinates
  85. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  86. # Single G74 or multi G75 quadrant for circular interpolation
  87. self.quad_re = re.compile(r'^G7([45]).*\*$')
  88. # Absolute/Relative G90/1 (OBSOLETE)
  89. self.absrel_re = re.compile(r'^G9([01])\*$')
  90. # flag to store if a conversion was done. It is needed because multiple units declarations can be found
  91. # in a Gerber file (normal or obsolete ones)
  92. self.conversion_done = False
  93. self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
  94. def parse_file(self, filename, follow=False):
  95. """
  96. Calls Gerber.parse_lines() with generator of lines
  97. read from the given file. Will split the lines if multiple
  98. statements are found in a single original line.
  99. The following line is split into two::
  100. G54D11*G36*
  101. First is ``G54D11*`` and seconds is ``G36*``.
  102. :param filename: Gerber file to parse.
  103. :type filename: str
  104. :param follow: If true, will not create polygons, just lines
  105. following the gerber path.
  106. :type follow: bool
  107. :return: None
  108. """
  109. with open(filename, 'r') as gfile:
  110. self.parse_lines([line.rstrip('\n') for line in gfile])
  111. def parse_lines(self, glines):
  112. """
  113. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  114. ``self.flashes``, ``self.regions`` and ``self.units``.
  115. :param glines: Gerber code as list of strings, each element being
  116. one line of the source file.
  117. :type glines: list
  118. :return: None
  119. :rtype: None
  120. """
  121. # Coordinates of the current path, each is [x, y]
  122. path = []
  123. # this is for temporary storage of solid geometry until it is added to poly_buffer
  124. geo_s = None
  125. # this is for temporary storage of follow geometry until it is added to follow_buffer
  126. geo_f = None
  127. # Polygons are stored here until there is a change in polarity.
  128. # Only then they are combined via cascaded_union and added or
  129. # subtracted from solid_geometry. This is ~100 times faster than
  130. # applying a union for every new polygon.
  131. poly_buffer = []
  132. # store here the follow geometry
  133. follow_buffer = []
  134. last_path_aperture = None
  135. current_aperture = None
  136. # 1,2 or 3 from "G01", "G02" or "G03"
  137. current_interpolation_mode = None
  138. # 1 or 2 from "D01" or "D02"
  139. # Note this is to support deprecated Gerber not putting
  140. # an operation code at the end of every coordinate line.
  141. current_operation_code = None
  142. # Current coordinates
  143. current_x = None
  144. current_y = None
  145. previous_x = None
  146. previous_y = None
  147. current_d = None
  148. # Absolute or Relative/Incremental coordinates
  149. # Not implemented
  150. absolute = True
  151. # How to interpret circular interpolation: SINGLE or MULTI
  152. quadrant_mode = None
  153. # Indicates we are parsing an aperture macro
  154. current_macro = None
  155. # Indicates the current polarity: D-Dark, C-Clear
  156. current_polarity = 'D'
  157. # If a region is being defined
  158. making_region = False
  159. # ### Parsing starts here ## ##
  160. line_num = 0
  161. gline = ""
  162. s_tol = float(self.app.defaults["gerber_simp_tolerance"])
  163. self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
  164. try:
  165. for gline in glines:
  166. if self.app.abort_flag:
  167. # graceful abort requested by the user
  168. raise FlatCAMApp.GracefulException
  169. line_num += 1
  170. self.source_file += gline + '\n'
  171. # Cleanup #
  172. gline = gline.strip(' \r\n')
  173. # log.debug("Line=%3s %s" % (line_num, gline))
  174. # ###################
  175. # Ignored lines #####
  176. # Comments #####
  177. # ###################
  178. match = self.comm_re.search(gline)
  179. if match:
  180. continue
  181. # ## Mode (IN/MM)
  182. # Example: %MOIN*%
  183. match = self.mode_re.search(gline)
  184. if match:
  185. self.units = match.group(1)
  186. log.debug("Gerber units found = %s" % self.units)
  187. # Changed for issue #80
  188. # self.convert_units(match.group(1))
  189. self.conversion_done = True
  190. continue
  191. # ############################################################# ##
  192. # Absolute/relative coordinates G90/1 OBSOLETE ######## ##
  193. # ##################################################### ##
  194. match = self.absrel_re.search(gline)
  195. if match:
  196. absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
  197. log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
  198. continue
  199. # ## G01 - Linear interpolation plus flashes
  200. # Operation code (D0x) missing is deprecated... oh well I will support it.
  201. # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
  202. match = self.lin_re.search(gline)
  203. if match:
  204. # Parse coordinates
  205. if match.group(2) is not None:
  206. linear_x = parse_number(match.group(2),
  207. self.int_digits, self.frac_digits, self.gerber_zeros)
  208. current_x = linear_x
  209. else:
  210. linear_x = current_x
  211. if match.group(3) is not None:
  212. linear_y = parse_number(match.group(3),
  213. self.int_digits, self.frac_digits, self.gerber_zeros)
  214. current_y = linear_y
  215. else:
  216. linear_y = current_y
  217. # Parse operation code
  218. if match.group(4) is not None:
  219. current_operation_code = int(match.group(4))
  220. # Pen down: add segment
  221. if current_operation_code == 1:
  222. # if linear_x or linear_y are None, ignore those
  223. if current_x is not None and current_y is not None:
  224. # only add the point if it's a new one otherwise skip it (harder to process)
  225. if path[-1] != [current_x, current_y]:
  226. path.append([current_x, current_y])
  227. if making_region is False:
  228. # if the aperture is rectangle then add a rectangular shape having as parameters the
  229. # coordinates of the start and end point and also the width and height
  230. # of the 'R' aperture
  231. try:
  232. if self.apertures[current_aperture]["type"] == 'R':
  233. width = self.apertures[current_aperture]['width']
  234. height = self.apertures[current_aperture]['height']
  235. minx = min(path[0][0], path[1][0]) - width / 2
  236. maxx = max(path[0][0], path[1][0]) + width / 2
  237. miny = min(path[0][1], path[1][1]) - height / 2
  238. maxy = max(path[0][1], path[1][1]) + height / 2
  239. log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
  240. geo_dict = dict()
  241. geo_f = Point([current_x, current_y])
  242. follow_buffer.append(geo_f)
  243. geo_dict['follow'] = geo_f
  244. geo_s = shply_box(minx, miny, maxx, maxy)
  245. if self.app.defaults['gerber_simplification']:
  246. poly_buffer.append(geo_s.simplify(s_tol))
  247. else:
  248. poly_buffer.append(geo_s)
  249. if self.is_lpc is True:
  250. geo_dict['clear'] = geo_s
  251. else:
  252. geo_dict['solid'] = geo_s
  253. if current_aperture not in self.apertures:
  254. self.apertures[current_aperture] = dict()
  255. if 'geometry' not in self.apertures[current_aperture]:
  256. self.apertures[current_aperture]['geometry'] = []
  257. self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
  258. except Exception as e:
  259. pass
  260. last_path_aperture = current_aperture
  261. # we do this for the case that a region is done without having defined any aperture
  262. if last_path_aperture is None:
  263. if '0' not in self.apertures:
  264. self.apertures['0'] = {}
  265. self.apertures['0']['type'] = 'REG'
  266. self.apertures['0']['size'] = 0.0
  267. self.apertures['0']['geometry'] = []
  268. last_path_aperture = '0'
  269. else:
  270. self.app.inform.emit('[WARNING] %s: %s' %
  271. (_("Coordinates missing, line ignored"), str(gline)))
  272. self.app.inform.emit('[WARNING_NOTCL] %s' %
  273. _("GERBER file might be CORRUPT. Check the file !!!"))
  274. elif current_operation_code == 2:
  275. if len(path) > 1:
  276. geo_s = None
  277. geo_dict = dict()
  278. # --- BUFFERED ---
  279. # this treats the case when we are storing geometry as paths only
  280. if making_region:
  281. # we do this for the case that a region is done without having defined any aperture
  282. if last_path_aperture is None:
  283. if '0' not in self.apertures:
  284. self.apertures['0'] = {}
  285. self.apertures['0']['type'] = 'REG'
  286. self.apertures['0']['size'] = 0.0
  287. self.apertures['0']['geometry'] = []
  288. last_path_aperture = '0'
  289. geo_f = Polygon()
  290. else:
  291. geo_f = LineString(path)
  292. try:
  293. if self.apertures[last_path_aperture]["type"] != 'R':
  294. if not geo_f.is_empty:
  295. follow_buffer.append(geo_f)
  296. geo_dict['follow'] = geo_f
  297. except Exception as e:
  298. log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
  299. if not geo_f.is_empty:
  300. follow_buffer.append(geo_f)
  301. geo_dict['follow'] = geo_f
  302. # this treats the case when we are storing geometry as solids
  303. if making_region:
  304. # we do this for the case that a region is done without having defined any aperture
  305. if last_path_aperture is None:
  306. if '0' not in self.apertures:
  307. self.apertures['0'] = {}
  308. self.apertures['0']['type'] = 'REG'
  309. self.apertures['0']['size'] = 0.0
  310. self.apertures['0']['geometry'] = []
  311. last_path_aperture = '0'
  312. try:
  313. geo_s = Polygon(path)
  314. except ValueError:
  315. log.warning("Problem %s %s" % (gline, line_num))
  316. self.app.inform.emit('[ERROR] %s: %s' %
  317. (_("Region does not have enough points. "
  318. "File will be processed but there are parser errors. "
  319. "Line number"), str(line_num)))
  320. else:
  321. if last_path_aperture is None:
  322. log.warning("No aperture defined for curent path. (%d)" % line_num)
  323. width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
  324. geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  325. try:
  326. if self.apertures[last_path_aperture]["type"] != 'R':
  327. if not geo_s.is_empty:
  328. if self.app.defaults['gerber_simplification']:
  329. poly_buffer.append(geo_s.simplify(s_tol))
  330. else:
  331. poly_buffer.append(geo_s)
  332. if self.is_lpc is True:
  333. geo_dict['clear'] = geo_s
  334. else:
  335. geo_dict['solid'] = geo_s
  336. except Exception as e:
  337. log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
  338. if self.app.defaults['gerber_simplification']:
  339. poly_buffer.append(geo_s.simplify(s_tol))
  340. else:
  341. poly_buffer.append(geo_s)
  342. if self.is_lpc is True:
  343. geo_dict['clear'] = geo_s
  344. else:
  345. geo_dict['solid'] = geo_s
  346. if last_path_aperture not in self.apertures:
  347. self.apertures[last_path_aperture] = dict()
  348. if 'geometry' not in self.apertures[last_path_aperture]:
  349. self.apertures[last_path_aperture]['geometry'] = []
  350. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  351. # if linear_x or linear_y are None, ignore those
  352. if linear_x is not None and linear_y is not None:
  353. path = [[linear_x, linear_y]] # Start new path
  354. else:
  355. self.app.inform.emit('[WARNING] %s: %s' %
  356. (_("Coordinates missing, line ignored"), str(gline)))
  357. self.app.inform.emit('[WARNING_NOTCL] %s' %
  358. _("GERBER file might be CORRUPT. Check the file !!!"))
  359. # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
  360. # are used in case that circular interpolation is encountered within the Gerber file
  361. current_x = linear_x
  362. current_y = linear_y
  363. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  364. continue
  365. # ## G02/3 - Circular interpolation
  366. # 2-clockwise, 3-counterclockwise
  367. # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
  368. match = self.circ_re.search(gline)
  369. if match:
  370. arcdir = [None, None, "cw", "ccw"]
  371. mode, circular_x, circular_y, i, j, d = match.groups()
  372. try:
  373. circular_x = parse_number(circular_x,
  374. self.int_digits, self.frac_digits, self.gerber_zeros)
  375. except Exception as e:
  376. circular_x = current_x
  377. try:
  378. circular_y = parse_number(circular_y,
  379. self.int_digits, self.frac_digits, self.gerber_zeros)
  380. except Exception as e:
  381. circular_y = current_y
  382. # According to Gerber specification i and j are not modal, which means that when i or j are missing,
  383. # they are to be interpreted as being zero
  384. try:
  385. i = parse_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
  386. except Exception as e:
  387. i = 0
  388. try:
  389. j = parse_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
  390. except Exception as e:
  391. j = 0
  392. if quadrant_mode is None:
  393. log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
  394. log.error(gline)
  395. continue
  396. if mode is None and current_interpolation_mode not in [2, 3]:
  397. log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  398. log.error(gline)
  399. continue
  400. elif mode is not None:
  401. current_interpolation_mode = int(mode)
  402. # Set operation code if provided
  403. if d is not None:
  404. current_operation_code = int(d)
  405. # Nothing created! Pen Up.
  406. if current_operation_code == 2:
  407. log.warning("Arc with D2. (%d)" % line_num)
  408. if len(path) > 1:
  409. geo_dict = dict()
  410. if last_path_aperture is None:
  411. log.warning("No aperture defined for curent path. (%d)" % line_num)
  412. # --- BUFFERED ---
  413. width = self.apertures[last_path_aperture]["size"]
  414. # this treats the case when we are storing geometry as paths
  415. geo_f = LineString(path)
  416. if not geo_f.is_empty:
  417. follow_buffer.append(geo_f)
  418. geo_dict['follow'] = geo_f
  419. # this treats the case when we are storing geometry as solids
  420. buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
  421. if not buffered.is_empty:
  422. if self.app.defaults['gerber_simplification']:
  423. poly_buffer.append(buffered.simplify(s_tol))
  424. else:
  425. poly_buffer.append(buffered)
  426. if self.is_lpc is True:
  427. geo_dict['clear'] = buffered
  428. else:
  429. geo_dict['solid'] = buffered
  430. if last_path_aperture not in self.apertures:
  431. self.apertures[last_path_aperture] = dict()
  432. if 'geometry' not in self.apertures[last_path_aperture]:
  433. self.apertures[last_path_aperture]['geometry'] = []
  434. self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  435. current_x = circular_x
  436. current_y = circular_y
  437. path = [[current_x, current_y]] # Start new path
  438. continue
  439. # Flash should not happen here
  440. if current_operation_code == 3:
  441. log.error("Trying to flash within arc. (%d)" % line_num)
  442. continue
  443. if quadrant_mode == 'MULTI':
  444. center = [i + current_x, j + current_y]
  445. radius = np.sqrt(i ** 2 + j ** 2)
  446. start = np.arctan2(-j, -i) # Start angle
  447. # Numerical errors might prevent start == stop therefore
  448. # we check ahead of time. This should result in a
  449. # 360 degree arc.
  450. if current_x == circular_x and current_y == circular_y:
  451. stop = start
  452. else:
  453. stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  454. this_arc = arc(center, radius, start, stop,
  455. arcdir[current_interpolation_mode],
  456. self.steps_per_circle)
  457. # The last point in the computed arc can have
  458. # numerical errors. The exact final point is the
  459. # specified (x, y). Replace.
  460. this_arc[-1] = (circular_x, circular_y)
  461. # Last point in path is current point
  462. # current_x = this_arc[-1][0]
  463. # current_y = this_arc[-1][1]
  464. current_x, current_y = circular_x, circular_y
  465. # Append
  466. path += this_arc
  467. last_path_aperture = current_aperture
  468. continue
  469. if quadrant_mode == 'SINGLE':
  470. center_candidates = [
  471. [i + current_x, j + current_y],
  472. [-i + current_x, j + current_y],
  473. [i + current_x, -j + current_y],
  474. [-i + current_x, -j + current_y]
  475. ]
  476. valid = False
  477. log.debug("I: %f J: %f" % (i, j))
  478. for center in center_candidates:
  479. radius = np.sqrt(i ** 2 + j ** 2)
  480. # Make sure radius to start is the same as radius to end.
  481. radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
  482. if radius2 < radius * 0.95 or radius2 > radius * 1.05:
  483. continue # Not a valid center.
  484. # Correct i and j and continue as with multi-quadrant.
  485. i = center[0] - current_x
  486. j = center[1] - current_y
  487. start = np.arctan2(-j, -i) # Start angle
  488. stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  489. angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  490. log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  491. (current_x, current_y, center[0], center[1], circular_x, circular_y))
  492. log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  493. (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
  494. angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
  495. if angle <= (np.pi + 1e-6) / 2:
  496. log.debug("########## ACCEPTING ARC ############")
  497. this_arc = arc(center, radius, start, stop,
  498. arcdir[current_interpolation_mode],
  499. self.steps_per_circle)
  500. # Replace with exact values
  501. this_arc[-1] = (circular_x, circular_y)
  502. # current_x = this_arc[-1][0]
  503. # current_y = this_arc[-1][1]
  504. current_x, current_y = circular_x, circular_y
  505. path += this_arc
  506. last_path_aperture = current_aperture
  507. valid = True
  508. break
  509. if valid:
  510. continue
  511. else:
  512. log.warning("Invalid arc in line %d." % line_num)
  513. # ## Line did not match any pattern. Warn user.
  514. log.warning("Line ignored (%d): %s" % (line_num, gline))
  515. # --- Apply buffer ---
  516. # this treats the case when we are storing geometry as paths
  517. self.follow_geometry = follow_buffer
  518. # this treats the case when we are storing geometry as solids
  519. if len(poly_buffer) == 0 and len(self.solid_geometry) == 0:
  520. log.error("Object is not Gerber file or empty. Aborting Object creation.")
  521. return 'fail'
  522. log.warning("Joining %d polygons." % len(poly_buffer))
  523. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
  524. if self.use_buffer_for_union:
  525. log.debug("Union by buffer...")
  526. new_poly = MultiPolygon(poly_buffer)
  527. if self.app.defaults["gerber_buffering"] == 'full':
  528. new_poly = new_poly.buffer(0.00000001)
  529. new_poly = new_poly.buffer(-0.00000001)
  530. log.warning("Union(buffer) done.")
  531. else:
  532. log.debug("Union by union()...")
  533. new_poly = cascaded_union(poly_buffer)
  534. new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
  535. log.warning("Union done.")
  536. if current_polarity == 'D':
  537. self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
  538. if new_poly.is_valid:
  539. self.solid_geometry = self.solid_geometry.union(new_poly)
  540. else:
  541. # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
  542. # loaded. Instead of applying a union I add to a list of polygons.
  543. final_poly = []
  544. try:
  545. for poly in new_poly:
  546. final_poly.append(poly)
  547. except TypeError:
  548. final_poly.append(new_poly)
  549. try:
  550. for poly in self.solid_geometry:
  551. final_poly.append(poly)
  552. except TypeError:
  553. final_poly.append(self.solid_geometry)
  554. self.solid_geometry = final_poly
  555. else:
  556. self.solid_geometry = self.solid_geometry.difference(new_poly)
  557. # init this for the following operations
  558. self.conversion_done = False
  559. except Exception as err:
  560. ex_type, ex, tb = sys.exc_info()
  561. traceback.print_tb(tb)
  562. # print traceback.format_exc()
  563. log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
  564. loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
  565. self.app.inform.emit('[ERROR] %s\n%s:' %
  566. (_("Gerber Parser ERROR"), loc))
  567. def create_geometry(self):
  568. """
  569. :rtype : None
  570. :return: None
  571. """
  572. pass
  573. def get_bounding_box(self, margin=0.0, rounded=False):
  574. """
  575. Creates and returns a rectangular polygon bounding at a distance of
  576. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  577. can optionally have rounded corners of radius equal to margin.
  578. :param margin: Distance to enlarge the rectangular bounding
  579. box in both positive and negative, x and y axes.
  580. :type margin: float
  581. :param rounded: Wether or not to have rounded corners.
  582. :type rounded: bool
  583. :return: The bounding box.
  584. :rtype: Shapely.Polygon
  585. """
  586. bbox = self.solid_geometry.envelope.buffer(margin)
  587. if not rounded:
  588. bbox = bbox.envelope
  589. return bbox
  590. def bounds(self):
  591. """
  592. Returns coordinates of rectangular bounds
  593. of Gerber geometry: (xmin, ymin, xmax, ymax).
  594. """
  595. # fixed issue of getting bounds only for one level lists of objects
  596. # now it can get bounds for nested lists of objects
  597. log.debug("parseGerber.Gerber.bounds()")
  598. if self.solid_geometry is None:
  599. log.debug("solid_geometry is None")
  600. return 0, 0, 0, 0
  601. def bounds_rec(obj):
  602. if type(obj) is list and type(obj) is not MultiPolygon:
  603. minx = np.Inf
  604. miny = np.Inf
  605. maxx = -np.Inf
  606. maxy = -np.Inf
  607. for k in obj:
  608. if type(k) is dict:
  609. for key in k:
  610. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  611. minx = min(minx, minx_)
  612. miny = min(miny, miny_)
  613. maxx = max(maxx, maxx_)
  614. maxy = max(maxy, maxy_)
  615. else:
  616. if not k.is_empty:
  617. try:
  618. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  619. except Exception as e:
  620. log.debug("camlib.Gerber.bounds() --> %s" % str(e))
  621. return
  622. minx = min(minx, minx_)
  623. miny = min(miny, miny_)
  624. maxx = max(maxx, maxx_)
  625. maxy = max(maxy, maxy_)
  626. return minx, miny, maxx, maxy
  627. else:
  628. # it's a Shapely object, return it's bounds
  629. return obj.bounds
  630. bounds_coords = bounds_rec(self.solid_geometry)
  631. return bounds_coords
  632. def convert_units(self, obj_units):
  633. """
  634. Converts the units of the object to ``units`` by scaling all
  635. the geometry appropriately. This call ``scale()``. Don't call
  636. it again in descendants.
  637. :param obj_units: "IN" or "MM"
  638. :type obj_units: str
  639. :return: Scaling factor resulting from unit change.
  640. :rtype: float
  641. """
  642. if obj_units.upper() == self.units.upper():
  643. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  644. return 1.0
  645. if obj_units.upper() == "MM":
  646. factor = 25.4
  647. log.debug("parseGerber.Gerber.convert_units() --> Factor: 25.4")
  648. elif obj_units.upper() == "IN":
  649. factor = 1 / 25.4
  650. log.debug("parseGerber.Gerber.convert_units() --> Factor: %s" % str(1 / 25.4))
  651. else:
  652. log.error("Unsupported units: %s" % str(obj_units))
  653. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  654. return 1.0
  655. self.units = obj_units
  656. self.file_units_factor = factor
  657. self.scale(factor, factor)
  658. return factor
  659. def scale(self, xfactor, yfactor=None, point=None):
  660. """
  661. Scales the objects' geometry on the XY plane by a given factor.
  662. These are:
  663. * ``buffered_paths``
  664. * ``flash_geometry``
  665. * ``solid_geometry``
  666. * ``regions``
  667. NOTE:
  668. Does not modify the data used to create these elements. If these
  669. are recreated, the scaling will be lost. This behavior was modified
  670. because of the complexity reached in this class.
  671. :param xfactor: Number by which to scale on X axis.
  672. :type xfactor: float
  673. :param yfactor: Number by which to scale on Y axis.
  674. :type yfactor: float
  675. :param point: reference point for scaling operation
  676. :rtype : None
  677. """
  678. log.debug("parseGerber.Gerber.scale()")
  679. try:
  680. xfactor = float(xfactor)
  681. except Exception:
  682. self.app.inform.emit('[ERROR_NOTCL] %s' %
  683. _("Scale factor has to be a number: integer or float."))
  684. return
  685. if yfactor is None:
  686. yfactor = xfactor
  687. else:
  688. try:
  689. yfactor = float(yfactor)
  690. except Exception:
  691. self.app.inform.emit('[ERROR_NOTCL] %s' %
  692. _("Scale factor has to be a number: integer or float."))
  693. return
  694. if xfactor == 0 and yfactor == 0:
  695. return
  696. if point is None:
  697. px = 0
  698. py = 0
  699. else:
  700. px, py = point
  701. # variables to display the percentage of work done
  702. self.geo_len = 0
  703. try:
  704. self.geo_len = len(self.solid_geometry)
  705. except TypeError:
  706. self.geo_len = 1
  707. self.old_disp_number = 0
  708. self.el_count = 0
  709. def scale_geom(obj):
  710. if type(obj) is list:
  711. new_obj = []
  712. for g in obj:
  713. new_obj.append(scale_geom(g))
  714. return new_obj
  715. else:
  716. try:
  717. self.el_count += 1
  718. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  719. if self.old_disp_number < disp_number <= 100:
  720. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  721. self.old_disp_number = disp_number
  722. return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
  723. except AttributeError:
  724. return obj
  725. self.solid_geometry = scale_geom(self.solid_geometry)
  726. self.follow_geometry = scale_geom(self.follow_geometry)
  727. # we need to scale the geometry stored in the Gerber apertures, too
  728. try:
  729. for apid in self.apertures:
  730. new_geometry = list()
  731. if 'geometry' in self.apertures[apid]:
  732. for geo_el in self.apertures[apid]['geometry']:
  733. new_geo_el = dict()
  734. if 'solid' in geo_el:
  735. new_geo_el['solid'] = scale_geom(geo_el['solid'])
  736. if 'follow' in geo_el:
  737. new_geo_el['follow'] = scale_geom(geo_el['follow'])
  738. if 'clear' in geo_el:
  739. new_geo_el['clear'] = scale_geom(geo_el['clear'])
  740. new_geometry.append(new_geo_el)
  741. self.apertures[apid]['geometry'] = deepcopy(new_geometry)
  742. try:
  743. if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
  744. self.apertures[apid]['width'] *= xfactor
  745. self.apertures[apid]['height'] *= xfactor
  746. elif str(self.apertures[apid]['type']) == 'P':
  747. self.apertures[apid]['diam'] *= xfactor
  748. self.apertures[apid]['nVertices'] *= xfactor
  749. except KeyError:
  750. pass
  751. try:
  752. if self.apertures[apid]['size'] is not None:
  753. self.apertures[apid]['size'] = float(self.apertures[apid]['size'] * xfactor)
  754. except KeyError:
  755. pass
  756. except Exception as e:
  757. log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
  758. return 'fail'
  759. self.app.inform.emit('[success] %s' % _("Gerber Scale done."))
  760. self.app.proc_container.new_text = ''
  761. # ## solid_geometry ???
  762. # It's a cascaded union of objects.
  763. # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  764. # factor, origin=(0, 0))
  765. # # Now buffered_paths, flash_geometry and solid_geometry
  766. # self.create_geometry()
  767. def offset(self, vect):
  768. """
  769. Offsets the objects' geometry on the XY plane by a given vector.
  770. These are:
  771. * ``buffered_paths``
  772. * ``flash_geometry``
  773. * ``solid_geometry``
  774. * ``regions``
  775. NOTE:
  776. Does not modify the data used to create these elements. If these
  777. are recreated, the scaling will be lost. This behavior was modified
  778. because of the complexity reached in this class.
  779. :param vect: (x, y) offset vector.
  780. :type vect: tuple
  781. :return: None
  782. """
  783. log.debug("parseGerber.Gerber.offset()")
  784. try:
  785. dx, dy = vect
  786. except TypeError:
  787. self.app.inform.emit('[ERROR_NOTCL] %s' %
  788. _("An (x,y) pair of values are needed. "
  789. "Probable you entered only one value in the Offset field."))
  790. return
  791. if dx == 0 and dy == 0:
  792. return
  793. # variables to display the percentage of work done
  794. self.geo_len = 0
  795. try:
  796. for __ in self.solid_geometry:
  797. self.geo_len += 1
  798. except TypeError:
  799. self.geo_len = 1
  800. self.old_disp_number = 0
  801. self.el_count = 0
  802. def offset_geom(obj):
  803. if type(obj) is list:
  804. new_obj = []
  805. for g in obj:
  806. new_obj.append(offset_geom(g))
  807. return new_obj
  808. else:
  809. try:
  810. self.el_count += 1
  811. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  812. if self.old_disp_number < disp_number <= 100:
  813. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  814. self.old_disp_number = disp_number
  815. return affinity.translate(obj, xoff=dx, yoff=dy)
  816. except AttributeError:
  817. return obj
  818. # ## Solid geometry
  819. self.solid_geometry = offset_geom(self.solid_geometry)
  820. self.follow_geometry = offset_geom(self.follow_geometry)
  821. # we need to offset the geometry stored in the Gerber apertures, too
  822. try:
  823. for apid in self.apertures:
  824. if 'geometry' in self.apertures[apid]:
  825. for geo_el in self.apertures[apid]['geometry']:
  826. if 'solid' in geo_el:
  827. geo_el['solid'] = offset_geom(geo_el['solid'])
  828. if 'follow' in geo_el:
  829. geo_el['follow'] = offset_geom(geo_el['follow'])
  830. if 'clear' in geo_el:
  831. geo_el['clear'] = offset_geom(geo_el['clear'])
  832. except Exception as e:
  833. log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
  834. return 'fail'
  835. self.app.inform.emit('[success] %s' %
  836. _("Gerber Offset done."))
  837. self.app.proc_container.new_text = ''
  838. def mirror(self, axis, point):
  839. """
  840. Mirrors the object around a specified axis passing through
  841. the given point. What is affected:
  842. * ``buffered_paths``
  843. * ``flash_geometry``
  844. * ``solid_geometry``
  845. * ``regions``
  846. NOTE:
  847. Does not modify the data used to create these elements. If these
  848. are recreated, the scaling will be lost. This behavior was modified
  849. because of the complexity reached in this class.
  850. :param axis: "X" or "Y" indicates around which axis to mirror.
  851. :type axis: str
  852. :param point: [x, y] point belonging to the mirror axis.
  853. :type point: list
  854. :return: None
  855. """
  856. log.debug("parseGerber.Gerber.mirror()")
  857. px, py = point
  858. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  859. # variables to display the percentage of work done
  860. self.geo_len = 0
  861. try:
  862. for __ in self.solid_geometry:
  863. self.geo_len += 1
  864. except TypeError:
  865. self.geo_len = 1
  866. self.old_disp_number = 0
  867. self.el_count = 0
  868. def mirror_geom(obj):
  869. if type(obj) is list:
  870. new_obj = []
  871. for g in obj:
  872. new_obj.append(mirror_geom(g))
  873. return new_obj
  874. else:
  875. try:
  876. self.el_count += 1
  877. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  878. if self.old_disp_number < disp_number <= 100:
  879. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  880. self.old_disp_number = disp_number
  881. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  882. except AttributeError:
  883. return obj
  884. self.solid_geometry = mirror_geom(self.solid_geometry)
  885. self.follow_geometry = mirror_geom(self.follow_geometry)
  886. # we need to mirror the geometry stored in the Gerber apertures, too
  887. try:
  888. for apid in self.apertures:
  889. if 'geometry' in self.apertures[apid]:
  890. for geo_el in self.apertures[apid]['geometry']:
  891. if 'solid' in geo_el:
  892. geo_el['solid'] = mirror_geom(geo_el['solid'])
  893. if 'follow' in geo_el:
  894. geo_el['follow'] = mirror_geom(geo_el['follow'])
  895. if 'clear' in geo_el:
  896. geo_el['clear'] = mirror_geom(geo_el['clear'])
  897. except Exception as e:
  898. log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
  899. return 'fail'
  900. self.app.inform.emit('[success] %s' %
  901. _("Gerber Mirror done."))
  902. self.app.proc_container.new_text = ''
  903. def skew(self, angle_x, angle_y, point):
  904. """
  905. Shear/Skew the geometries of an object by angles along x and y dimensions.
  906. Parameters
  907. ----------
  908. angle_x, angle_y : float, float
  909. The shear angle(s) for the x and y axes respectively. These can be
  910. specified in either degrees (default) or radians by setting
  911. use_radians=True.
  912. See shapely manual for more information:
  913. http://toblerity.org/shapely/manual.html#affine-transformations
  914. :param angle_x: the angle on X axis for skewing
  915. :param angle_y: the angle on Y axis for skewing
  916. :param point: reference point for skewing operation
  917. :return None
  918. """
  919. log.debug("parseGerber.Gerber.skew()")
  920. px, py = point
  921. if angle_x == 0 and angle_y == 0:
  922. return
  923. # variables to display the percentage of work done
  924. self.geo_len = 0
  925. try:
  926. self.geo_len = len(self.solid_geometry)
  927. except TypeError:
  928. self.geo_len = 1
  929. self.old_disp_number = 0
  930. self.el_count = 0
  931. def skew_geom(obj):
  932. if type(obj) is list:
  933. new_obj = []
  934. for g in obj:
  935. new_obj.append(skew_geom(g))
  936. return new_obj
  937. else:
  938. try:
  939. self.el_count += 1
  940. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  941. if self.old_disp_number < disp_number <= 100:
  942. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  943. self.old_disp_number = disp_number
  944. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  945. except AttributeError:
  946. return obj
  947. self.solid_geometry = skew_geom(self.solid_geometry)
  948. self.follow_geometry = skew_geom(self.follow_geometry)
  949. # we need to skew the geometry stored in the Gerber apertures, too
  950. try:
  951. for apid in self.apertures:
  952. if 'geometry' in self.apertures[apid]:
  953. for geo_el in self.apertures[apid]['geometry']:
  954. if 'solid' in geo_el:
  955. geo_el['solid'] = skew_geom(geo_el['solid'])
  956. if 'follow' in geo_el:
  957. geo_el['follow'] = skew_geom(geo_el['follow'])
  958. if 'clear' in geo_el:
  959. geo_el['clear'] = skew_geom(geo_el['clear'])
  960. except Exception as e:
  961. log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
  962. return 'fail'
  963. self.app.inform.emit('[success] %s' % _("Gerber Skew done."))
  964. self.app.proc_container.new_text = ''
  965. def rotate(self, angle, point):
  966. """
  967. Rotate an object by a given angle around given coords (point)
  968. :param angle:
  969. :param point:
  970. :return:
  971. """
  972. log.debug("parseGerber.Gerber.rotate()")
  973. px, py = point
  974. if angle == 0:
  975. return
  976. # variables to display the percentage of work done
  977. self.geo_len = 0
  978. try:
  979. for __ in self.solid_geometry:
  980. self.geo_len += 1
  981. except TypeError:
  982. self.geo_len = 1
  983. self.old_disp_number = 0
  984. self.el_count = 0
  985. def rotate_geom(obj):
  986. if type(obj) is list:
  987. new_obj = []
  988. for g in obj:
  989. new_obj.append(rotate_geom(g))
  990. return new_obj
  991. else:
  992. try:
  993. self.el_count += 1
  994. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  995. if self.old_disp_number < disp_number <= 100:
  996. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  997. self.old_disp_number = disp_number
  998. return affinity.rotate(obj, angle, origin=(px, py))
  999. except AttributeError:
  1000. return obj
  1001. self.solid_geometry = rotate_geom(self.solid_geometry)
  1002. self.follow_geometry = rotate_geom(self.follow_geometry)
  1003. # we need to rotate the geometry stored in the Gerber apertures, too
  1004. try:
  1005. for apid in self.apertures:
  1006. if 'geometry' in self.apertures[apid]:
  1007. for geo_el in self.apertures[apid]['geometry']:
  1008. if 'solid' in geo_el:
  1009. geo_el['solid'] = rotate_geom(geo_el['solid'])
  1010. if 'follow' in geo_el:
  1011. geo_el['follow'] = rotate_geom(geo_el['follow'])
  1012. if 'clear' in geo_el:
  1013. geo_el['clear'] = rotate_geom(geo_el['clear'])
  1014. except Exception as e:
  1015. log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
  1016. return 'fail'
  1017. self.app.inform.emit('[success] %s' %
  1018. _("Gerber Rotate done."))
  1019. self.app.proc_container.new_text = ''
  1020. def parse_number(strnumber):
  1021. """
  1022. Parse a single number of HPGL2 coordinates.
  1023. :param strnumber: String containing a number
  1024. from a coordinate data block, possibly with a leading sign.
  1025. :type strnumber: str
  1026. :return: The number in floating point.
  1027. :rtype: float
  1028. """
  1029. return float(strnumber) * 40.0 # in milimeters