ParseHPGL2.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. # ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian 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, MultiLineString
  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 = 'MM'
  50. # storage for the tools
  51. self.tools = dict()
  52. self.default_data = dict()
  53. self.default_data.update({
  54. "name": '_ncc',
  55. "plot": self.app.defaults["geometry_plot"],
  56. "cutz": self.app.defaults["geometry_cutz"],
  57. "vtipdia": self.app.defaults["geometry_vtipdia"],
  58. "vtipangle": self.app.defaults["geometry_vtipangle"],
  59. "travelz": self.app.defaults["geometry_travelz"],
  60. "feedrate": self.app.defaults["geometry_feedrate"],
  61. "feedrate_z": self.app.defaults["geometry_feedrate_z"],
  62. "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
  63. "dwell": self.app.defaults["geometry_dwell"],
  64. "dwelltime": self.app.defaults["geometry_dwelltime"],
  65. "multidepth": self.app.defaults["geometry_multidepth"],
  66. "ppname_g": self.app.defaults["geometry_ppname_g"],
  67. "depthperpass": self.app.defaults["geometry_depthperpass"],
  68. "extracut": self.app.defaults["geometry_extracut"],
  69. "extracut_length": self.app.defaults["geometry_extracut_length"],
  70. "toolchange": self.app.defaults["geometry_toolchange"],
  71. "toolchangez": self.app.defaults["geometry_toolchangez"],
  72. "endz": self.app.defaults["geometry_endz"],
  73. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  74. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  75. "startz": self.app.defaults["geometry_startz"],
  76. "tooldia": self.app.defaults["tools_painttooldia"],
  77. "paintmargin": self.app.defaults["tools_paintmargin"],
  78. "paintmethod": self.app.defaults["tools_paintmethod"],
  79. "selectmethod": self.app.defaults["tools_selectmethod"],
  80. "pathconnect": self.app.defaults["tools_pathconnect"],
  81. "paintcontour": self.app.defaults["tools_paintcontour"],
  82. "paintoverlap": self.app.defaults["tools_paintoverlap"],
  83. "nccoverlap": self.app.defaults["tools_nccoverlap"],
  84. "nccmargin": self.app.defaults["tools_nccmargin"],
  85. "nccmethod": self.app.defaults["tools_nccmethod"],
  86. "nccconnect": self.app.defaults["tools_nccconnect"],
  87. "ncccontour": self.app.defaults["tools_ncccontour"],
  88. "nccrest": self.app.defaults["tools_nccrest"]
  89. })
  90. # flag to be set True when tool is detected
  91. self.tool_detected = False
  92. # will store the geometry's as solids
  93. self.solid_geometry = None
  94. # will store the geometry's as paths
  95. self.follow_geometry = []
  96. self.source_file = ''
  97. # Attributes to be included in serialization
  98. # Always append to it because it carries contents
  99. # from Geometry.
  100. self.ser_attrs += ['solid_geometry', 'follow_geometry', 'source_file']
  101. # ### Parser patterns ## ##
  102. # comment
  103. self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
  104. # absolute move to x, y
  105. self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d+?),?\s*(-?\d+\.?\d+?)*;?$")
  106. # relative move to x, y
  107. self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
  108. # pen position
  109. self.pen_re = re.compile(r"^(P[U|D]);?$")
  110. # Initialize
  111. self.initialize_re = re.compile(r'^(IN);?$')
  112. # select pen
  113. self.sp_re = re.compile(r'SP(\d);?$')
  114. self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
  115. self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
  116. # G01... - Linear interpolation plus flashes with coordinates
  117. # Operation code (D0x) missing is deprecated... oh well I will support it.
  118. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  119. # G02/3... - Circular interpolation with coordinates
  120. # 2-clockwise, 3-counterclockwise
  121. # Operation code (D0x) missing is deprecated... oh well I will support it.
  122. # Optional start with G02 or G03, optional end with D01 or D02 with
  123. # optional coordinates but at least one in any order.
  124. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' +
  125. '?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  126. # Absolute/Relative G90/1 (OBSOLETE)
  127. self.absrel_re = re.compile(r'^G9([01])\*$')
  128. # flag to store if a conversion was done. It is needed because multiple units declarations can be found
  129. # in a Gerber file (normal or obsolete ones)
  130. self.conversion_done = False
  131. self.in_header = None
  132. def parse_file(self, filename):
  133. """
  134. :param filename: HPGL2 file to parse.
  135. :type filename: str
  136. :return: None
  137. """
  138. with open(filename, 'r') as gfile:
  139. self.parse_lines([line.rstrip('\n') for line in gfile])
  140. def parse_lines(self, glines):
  141. """
  142. Main HPGL2 parser.
  143. :param glines: HPGL2 code as list of strings, each element being
  144. one line of the source file.
  145. :type glines: list
  146. :return: None
  147. :rtype: None
  148. """
  149. # Coordinates of the current path, each is [x, y]
  150. path = list()
  151. geo_buffer = []
  152. # Current coordinates
  153. current_x = None
  154. current_y = None
  155. previous_x = None
  156. previous_y = None
  157. # store the pen (tool) status
  158. pen_status = 'up'
  159. # store the current tool here
  160. current_tool = None
  161. # ### Parsing starts here ## ##
  162. line_num = 0
  163. gline = ""
  164. self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
  165. try:
  166. for gline in glines:
  167. if self.app.abort_flag:
  168. # graceful abort requested by the user
  169. raise FlatCAMApp.GracefulException
  170. line_num += 1
  171. self.source_file += gline + '\n'
  172. # Cleanup #
  173. gline = gline.strip(' \r\n')
  174. # log.debug("Line=%3s %s" % (line_num, gline))
  175. # ###################
  176. # Ignored lines #####
  177. # Comments #####
  178. # ###################
  179. match = self.comment_re.search(gline)
  180. if match:
  181. log.debug(str(match.group(1)))
  182. continue
  183. # #####################################################
  184. # Absolute/relative coordinates G90/1 OBSOLETE ########
  185. # #####################################################
  186. match = self.absrel_re.search(gline)
  187. if match:
  188. absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
  189. log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
  190. continue
  191. # search for the initialization
  192. match = self.initialize_re.search(gline)
  193. if match:
  194. self.in_header = False
  195. continue
  196. if self.in_header is False:
  197. # tools detection
  198. match = self.sp_re.search(gline)
  199. if match:
  200. tool = match.group(1)
  201. # self.tools[tool] = dict()
  202. self.tools.update({
  203. tool: {
  204. 'tooldia': float('%.*f' %
  205. (
  206. self.decimals,
  207. float(self.app.defaults['geometry_cnctooldia'])
  208. )
  209. ),
  210. 'offset': 'Path',
  211. 'offset_value': 0.0,
  212. 'type': 'Iso',
  213. 'tool_type': 'C1',
  214. 'data': deepcopy(self.default_data),
  215. 'solid_geometry': list()
  216. }
  217. })
  218. if current_tool:
  219. if path:
  220. geo = LineString(path)
  221. self.tools[current_tool]['solid_geometry'].append(geo)
  222. geo_buffer.append(geo)
  223. path[:] = []
  224. current_tool = tool
  225. continue
  226. # pen status detection
  227. match = self.pen_re.search(gline)
  228. if match:
  229. pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
  230. continue
  231. # linear move
  232. match = self.abs_move_re.search(gline)
  233. if match:
  234. # Parse coordinates
  235. if match.group(1) is not None:
  236. linear_x = parse_number(match.group(1))
  237. current_x = linear_x
  238. else:
  239. linear_x = current_x
  240. if match.group(2) is not None:
  241. linear_y = parse_number(match.group(2))
  242. current_y = linear_y
  243. else:
  244. linear_y = current_y
  245. # Pen down: add segment
  246. if pen_status == 'down':
  247. # if linear_x or linear_y are None, ignore those
  248. if current_x is not None and current_y is not None:
  249. # only add the point if it's a new one otherwise skip it (harder to process)
  250. if path[-1] != [current_x, current_y]:
  251. path.append([current_x, current_y])
  252. else:
  253. self.app.inform.emit('[WARNING] %s: %s' %
  254. (_("Coordinates missing, line ignored"), str(gline)))
  255. elif pen_status == 'up':
  256. if len(path) > 1:
  257. geo = LineString(path)
  258. self.tools[current_tool]['solid_geometry'].append(geo)
  259. geo_buffer.append(geo)
  260. # if linear_x or linear_y are None, ignore those
  261. if linear_x is not None and linear_y is not None:
  262. path = [[linear_x, linear_y]] # Start new path
  263. else:
  264. self.app.inform.emit('[WARNING] %s: %s' %
  265. (_("Coordinates missing, line ignored"), str(gline)))
  266. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  267. continue
  268. # ## Circular interpolation
  269. # -clockwise,
  270. # -counterclockwise
  271. match = self.circ_re.search(gline)
  272. # if match:
  273. # arcdir = [None, None, "cw", "ccw"]
  274. #
  275. # mode, circular_x, circular_y, i, j, d = match.groups()
  276. #
  277. # try:
  278. # circular_x = parse_number(circular_x)
  279. # except Exception as e:
  280. # circular_x = current_x
  281. #
  282. # try:
  283. # circular_y = parse_number(circular_y)
  284. # except Exception as e:
  285. # circular_y = current_y
  286. #
  287. # try:
  288. # i = parse_number(i)
  289. # except Exception as e:
  290. # i = 0
  291. #
  292. # try:
  293. # j = parse_number(j)
  294. # except Exception as e:
  295. # j = 0
  296. #
  297. # if mode is None and current_interpolation_mode not in [2, 3]:
  298. # log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  299. # log.error(gline)
  300. # continue
  301. # elif mode is not None:
  302. # current_interpolation_mode = int(mode)
  303. #
  304. # # Set operation code if provided
  305. # if d is not None:
  306. # current_operation_code = int(d)
  307. #
  308. # # Nothing created! Pen Up.
  309. # if current_operation_code == 2:
  310. # log.warning("Arc with D2. (%d)" % line_num)
  311. # if len(path) > 1:
  312. # geo_dict = dict()
  313. #
  314. # if last_path_aperture is None:
  315. # log.warning("No aperture defined for curent path. (%d)" % line_num)
  316. #
  317. # # --- BUFFERED ---
  318. # width = self.apertures[last_path_aperture]["size"]
  319. #
  320. # # this treats the case when we are storing geometry as paths
  321. # geo_f = LineString(path)
  322. # if not geo_f.is_empty:
  323. # geo_dict['follow'] = geo_f
  324. #
  325. # # this treats the case when we are storing geometry as solids
  326. # buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
  327. #
  328. # if last_path_aperture not in self.apertures:
  329. # self.apertures[last_path_aperture] = dict()
  330. # if 'geometry' not in self.apertures[last_path_aperture]:
  331. # self.apertures[last_path_aperture]['geometry'] = []
  332. # self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  333. #
  334. # current_x = circular_x
  335. # current_y = circular_y
  336. # path = [[current_x, current_y]] # Start new path
  337. # continue
  338. #
  339. # # Flash should not happen here
  340. # if current_operation_code == 3:
  341. # log.error("Trying to flash within arc. (%d)" % line_num)
  342. # continue
  343. #
  344. # if quadrant_mode == 'MULTI':
  345. # center = [i + current_x, j + current_y]
  346. # radius = np.sqrt(i ** 2 + j ** 2)
  347. # start = np.arctan2(-j, -i) # Start angle
  348. # # Numerical errors might prevent start == stop therefore
  349. # # we check ahead of time. This should result in a
  350. # # 360 degree arc.
  351. # if current_x == circular_x and current_y == circular_y:
  352. # stop = start
  353. # else:
  354. # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  355. #
  356. # this_arc = arc(center, radius, start, stop,
  357. # arcdir[current_interpolation_mode],
  358. # self.steps_per_circle)
  359. #
  360. # # The last point in the computed arc can have
  361. # # numerical errors. The exact final point is the
  362. # # specified (x, y). Replace.
  363. # this_arc[-1] = (circular_x, circular_y)
  364. #
  365. # # Last point in path is current point
  366. # # current_x = this_arc[-1][0]
  367. # # current_y = this_arc[-1][1]
  368. # current_x, current_y = circular_x, circular_y
  369. #
  370. # # Append
  371. # path += this_arc
  372. # last_path_aperture = current_aperture
  373. #
  374. # continue
  375. #
  376. # if quadrant_mode == 'SINGLE':
  377. #
  378. # center_candidates = [
  379. # [i + current_x, j + current_y],
  380. # [-i + current_x, j + current_y],
  381. # [i + current_x, -j + current_y],
  382. # [-i + current_x, -j + current_y]
  383. # ]
  384. #
  385. # valid = False
  386. # log.debug("I: %f J: %f" % (i, j))
  387. # for center in center_candidates:
  388. # radius = np.sqrt(i ** 2 + j ** 2)
  389. #
  390. # # Make sure radius to start is the same as radius to end.
  391. # radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
  392. # if radius2 < radius * 0.95 or radius2 > radius * 1.05:
  393. # continue # Not a valid center.
  394. #
  395. # # Correct i and j and continue as with multi-quadrant.
  396. # i = center[0] - current_x
  397. # j = center[1] - current_y
  398. #
  399. # start = np.arctan2(-j, -i) # Start angle
  400. # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  401. # angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  402. # log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  403. # (current_x, current_y, center[0], center[1], circular_x, circular_y))
  404. # log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  405. # (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
  406. # angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
  407. #
  408. # if angle <= (np.pi + 1e-6) / 2:
  409. # log.debug("########## ACCEPTING ARC ############")
  410. # this_arc = arc(center, radius, start, stop,
  411. # arcdir[current_interpolation_mode],
  412. # self.steps_per_circle)
  413. #
  414. # # Replace with exact values
  415. # this_arc[-1] = (circular_x, circular_y)
  416. #
  417. # # current_x = this_arc[-1][0]
  418. # # current_y = this_arc[-1][1]
  419. # current_x, current_y = circular_x, circular_y
  420. #
  421. # path += this_arc
  422. # last_path_aperture = current_aperture
  423. # valid = True
  424. # break
  425. #
  426. # if valid:
  427. # continue
  428. # else:
  429. # log.warning("Invalid arc in line %d." % line_num)
  430. # ## Line did not match any pattern. Warn user.
  431. log.warning("Line ignored (%d): %s" % (line_num, gline))
  432. if len(geo_buffer) == 0 and len(self.solid_geometry) == 0:
  433. log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
  434. return 'fail'
  435. log.warning("Joining %d polygons." % len(geo_buffer))
  436. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
  437. new_poly = unary_union(geo_buffer)
  438. self.solid_geometry = new_poly
  439. except Exception as err:
  440. ex_type, ex, tb = sys.exc_info()
  441. traceback.print_tb(tb)
  442. # print traceback.format_exc()
  443. log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
  444. loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
  445. self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
  446. def create_geometry(self):
  447. """
  448. :rtype : None
  449. :return: None
  450. """
  451. pass
  452. def get_bounding_box(self, margin=0.0, rounded=False):
  453. """
  454. Creates and returns a rectangular polygon bounding at a distance of
  455. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  456. can optionally have rounded corners of radius equal to margin.
  457. :param margin: Distance to enlarge the rectangular bounding
  458. box in both positive and negative, x and y axes.
  459. :type margin: float
  460. :param rounded: Wether or not to have rounded corners.
  461. :type rounded: bool
  462. :return: The bounding box.
  463. :rtype: Shapely.Polygon
  464. """
  465. bbox = self.solid_geometry.envelope.buffer(margin)
  466. if not rounded:
  467. bbox = bbox.envelope
  468. return bbox
  469. def bounds(self):
  470. """
  471. Returns coordinates of rectangular bounds
  472. of Gerber geometry: (xmin, ymin, xmax, ymax).
  473. """
  474. # fixed issue of getting bounds only for one level lists of objects
  475. # now it can get bounds for nested lists of objects
  476. log.debug("parseGerber.Gerber.bounds()")
  477. if self.solid_geometry is None:
  478. log.debug("solid_geometry is None")
  479. return 0, 0, 0, 0
  480. def bounds_rec(obj):
  481. if type(obj) is list and type(obj) is not MultiPolygon:
  482. minx = np.Inf
  483. miny = np.Inf
  484. maxx = -np.Inf
  485. maxy = -np.Inf
  486. for k in obj:
  487. if type(k) is dict:
  488. for key in k:
  489. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  490. minx = min(minx, minx_)
  491. miny = min(miny, miny_)
  492. maxx = max(maxx, maxx_)
  493. maxy = max(maxy, maxy_)
  494. else:
  495. if not k.is_empty:
  496. try:
  497. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  498. except Exception as e:
  499. log.debug("camlib.Gerber.bounds() --> %s" % str(e))
  500. return
  501. minx = min(minx, minx_)
  502. miny = min(miny, miny_)
  503. maxx = max(maxx, maxx_)
  504. maxy = max(maxy, maxy_)
  505. return minx, miny, maxx, maxy
  506. else:
  507. # it's a Shapely object, return it's bounds
  508. return obj.bounds
  509. bounds_coords = bounds_rec(self.solid_geometry)
  510. return bounds_coords
  511. def convert_units(self, obj_units):
  512. """
  513. Converts the units of the object to ``units`` by scaling all
  514. the geometry appropriately. This call ``scale()``. Don't call
  515. it again in descendants.
  516. :param obj_units: "IN" or "MM"
  517. :type obj_units: str
  518. :return: Scaling factor resulting from unit change.
  519. :rtype: float
  520. """
  521. if obj_units.upper() == self.units.upper():
  522. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  523. return 1.0
  524. if obj_units.upper() == "MM":
  525. factor = 25.4
  526. log.debug("parseGerber.Gerber.convert_units() --> Factor: 25.4")
  527. elif obj_units.upper() == "IN":
  528. factor = 1 / 25.4
  529. log.debug("parseGerber.Gerber.convert_units() --> Factor: %s" % str(1 / 25.4))
  530. else:
  531. log.error("Unsupported units: %s" % str(obj_units))
  532. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  533. return 1.0
  534. self.units = obj_units
  535. self.file_units_factor = factor
  536. self.scale(factor, factor)
  537. return factor
  538. def scale(self, xfactor, yfactor=None, point=None):
  539. """
  540. Scales the objects' geometry on the XY plane by a given factor.
  541. These are:
  542. * ``buffered_paths``
  543. * ``flash_geometry``
  544. * ``solid_geometry``
  545. * ``regions``
  546. NOTE:
  547. Does not modify the data used to create these elements. If these
  548. are recreated, the scaling will be lost. This behavior was modified
  549. because of the complexity reached in this class.
  550. :param xfactor: Number by which to scale on X axis.
  551. :type xfactor: float
  552. :param yfactor: Number by which to scale on Y axis.
  553. :type yfactor: float
  554. :param point: reference point for scaling operation
  555. :rtype : None
  556. """
  557. log.debug("parseGerber.Gerber.scale()")
  558. try:
  559. xfactor = float(xfactor)
  560. except Exception:
  561. self.app.inform.emit('[ERROR_NOTCL] %s' %
  562. _("Scale factor has to be a number: integer or float."))
  563. return
  564. if yfactor is None:
  565. yfactor = xfactor
  566. else:
  567. try:
  568. yfactor = float(yfactor)
  569. except Exception:
  570. self.app.inform.emit('[ERROR_NOTCL] %s' %
  571. _("Scale factor has to be a number: integer or float."))
  572. return
  573. if xfactor == 0 and yfactor == 0:
  574. return
  575. if point is None:
  576. px = 0
  577. py = 0
  578. else:
  579. px, py = point
  580. # variables to display the percentage of work done
  581. self.geo_len = 0
  582. try:
  583. self.geo_len = len(self.solid_geometry)
  584. except TypeError:
  585. self.geo_len = 1
  586. self.old_disp_number = 0
  587. self.el_count = 0
  588. def scale_geom(obj):
  589. if type(obj) is list:
  590. new_obj = []
  591. for g in obj:
  592. new_obj.append(scale_geom(g))
  593. return new_obj
  594. else:
  595. try:
  596. self.el_count += 1
  597. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  598. if self.old_disp_number < disp_number <= 100:
  599. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  600. self.old_disp_number = disp_number
  601. return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
  602. except AttributeError:
  603. return obj
  604. self.solid_geometry = scale_geom(self.solid_geometry)
  605. self.follow_geometry = scale_geom(self.follow_geometry)
  606. # we need to scale the geometry stored in the Gerber apertures, too
  607. try:
  608. for apid in self.apertures:
  609. new_geometry = list()
  610. if 'geometry' in self.apertures[apid]:
  611. for geo_el in self.apertures[apid]['geometry']:
  612. new_geo_el = dict()
  613. if 'solid' in geo_el:
  614. new_geo_el['solid'] = scale_geom(geo_el['solid'])
  615. if 'follow' in geo_el:
  616. new_geo_el['follow'] = scale_geom(geo_el['follow'])
  617. if 'clear' in geo_el:
  618. new_geo_el['clear'] = scale_geom(geo_el['clear'])
  619. new_geometry.append(new_geo_el)
  620. self.apertures[apid]['geometry'] = deepcopy(new_geometry)
  621. try:
  622. if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
  623. self.apertures[apid]['width'] *= xfactor
  624. self.apertures[apid]['height'] *= xfactor
  625. elif str(self.apertures[apid]['type']) == 'P':
  626. self.apertures[apid]['diam'] *= xfactor
  627. self.apertures[apid]['nVertices'] *= xfactor
  628. except KeyError:
  629. pass
  630. try:
  631. if self.apertures[apid]['size'] is not None:
  632. self.apertures[apid]['size'] = float(self.apertures[apid]['size'] * xfactor)
  633. except KeyError:
  634. pass
  635. except Exception as e:
  636. log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
  637. return 'fail'
  638. self.app.inform.emit('[success] %s' % _("Gerber Scale done."))
  639. self.app.proc_container.new_text = ''
  640. # ## solid_geometry ???
  641. # It's a cascaded union of objects.
  642. # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  643. # factor, origin=(0, 0))
  644. # # Now buffered_paths, flash_geometry and solid_geometry
  645. # self.create_geometry()
  646. def offset(self, vect):
  647. """
  648. Offsets the objects' geometry on the XY plane by a given vector.
  649. These are:
  650. * ``buffered_paths``
  651. * ``flash_geometry``
  652. * ``solid_geometry``
  653. * ``regions``
  654. NOTE:
  655. Does not modify the data used to create these elements. If these
  656. are recreated, the scaling will be lost. This behavior was modified
  657. because of the complexity reached in this class.
  658. :param vect: (x, y) offset vector.
  659. :type vect: tuple
  660. :return: None
  661. """
  662. log.debug("parseGerber.Gerber.offset()")
  663. try:
  664. dx, dy = vect
  665. except TypeError:
  666. self.app.inform.emit('[ERROR_NOTCL] %s' %
  667. _("An (x,y) pair of values are needed. "
  668. "Probable you entered only one value in the Offset field."))
  669. return
  670. if dx == 0 and dy == 0:
  671. return
  672. # variables to display the percentage of work done
  673. self.geo_len = 0
  674. try:
  675. for __ in self.solid_geometry:
  676. self.geo_len += 1
  677. except TypeError:
  678. self.geo_len = 1
  679. self.old_disp_number = 0
  680. self.el_count = 0
  681. def offset_geom(obj):
  682. if type(obj) is list:
  683. new_obj = []
  684. for g in obj:
  685. new_obj.append(offset_geom(g))
  686. return new_obj
  687. else:
  688. try:
  689. self.el_count += 1
  690. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  691. if self.old_disp_number < disp_number <= 100:
  692. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  693. self.old_disp_number = disp_number
  694. return affinity.translate(obj, xoff=dx, yoff=dy)
  695. except AttributeError:
  696. return obj
  697. # ## Solid geometry
  698. self.solid_geometry = offset_geom(self.solid_geometry)
  699. self.follow_geometry = offset_geom(self.follow_geometry)
  700. # we need to offset the geometry stored in the Gerber apertures, too
  701. try:
  702. for apid in self.apertures:
  703. if 'geometry' in self.apertures[apid]:
  704. for geo_el in self.apertures[apid]['geometry']:
  705. if 'solid' in geo_el:
  706. geo_el['solid'] = offset_geom(geo_el['solid'])
  707. if 'follow' in geo_el:
  708. geo_el['follow'] = offset_geom(geo_el['follow'])
  709. if 'clear' in geo_el:
  710. geo_el['clear'] = offset_geom(geo_el['clear'])
  711. except Exception as e:
  712. log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
  713. return 'fail'
  714. self.app.inform.emit('[success] %s' %
  715. _("Gerber Offset done."))
  716. self.app.proc_container.new_text = ''
  717. def mirror(self, axis, point):
  718. """
  719. Mirrors the object around a specified axis passing through
  720. the given point. What is affected:
  721. * ``buffered_paths``
  722. * ``flash_geometry``
  723. * ``solid_geometry``
  724. * ``regions``
  725. NOTE:
  726. Does not modify the data used to create these elements. If these
  727. are recreated, the scaling will be lost. This behavior was modified
  728. because of the complexity reached in this class.
  729. :param axis: "X" or "Y" indicates around which axis to mirror.
  730. :type axis: str
  731. :param point: [x, y] point belonging to the mirror axis.
  732. :type point: list
  733. :return: None
  734. """
  735. log.debug("parseGerber.Gerber.mirror()")
  736. px, py = point
  737. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  738. # variables to display the percentage of work done
  739. self.geo_len = 0
  740. try:
  741. for __ in self.solid_geometry:
  742. self.geo_len += 1
  743. except TypeError:
  744. self.geo_len = 1
  745. self.old_disp_number = 0
  746. self.el_count = 0
  747. def mirror_geom(obj):
  748. if type(obj) is list:
  749. new_obj = []
  750. for g in obj:
  751. new_obj.append(mirror_geom(g))
  752. return new_obj
  753. else:
  754. try:
  755. self.el_count += 1
  756. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  757. if self.old_disp_number < disp_number <= 100:
  758. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  759. self.old_disp_number = disp_number
  760. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  761. except AttributeError:
  762. return obj
  763. self.solid_geometry = mirror_geom(self.solid_geometry)
  764. self.follow_geometry = mirror_geom(self.follow_geometry)
  765. # we need to mirror the geometry stored in the Gerber apertures, too
  766. try:
  767. for apid in self.apertures:
  768. if 'geometry' in self.apertures[apid]:
  769. for geo_el in self.apertures[apid]['geometry']:
  770. if 'solid' in geo_el:
  771. geo_el['solid'] = mirror_geom(geo_el['solid'])
  772. if 'follow' in geo_el:
  773. geo_el['follow'] = mirror_geom(geo_el['follow'])
  774. if 'clear' in geo_el:
  775. geo_el['clear'] = mirror_geom(geo_el['clear'])
  776. except Exception as e:
  777. log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
  778. return 'fail'
  779. self.app.inform.emit('[success] %s' %
  780. _("Gerber Mirror done."))
  781. self.app.proc_container.new_text = ''
  782. def skew(self, angle_x, angle_y, point):
  783. """
  784. Shear/Skew the geometries of an object by angles along x and y dimensions.
  785. Parameters
  786. ----------
  787. angle_x, angle_y : float, float
  788. The shear angle(s) for the x and y axes respectively. These can be
  789. specified in either degrees (default) or radians by setting
  790. use_radians=True.
  791. See shapely manual for more information:
  792. http://toblerity.org/shapely/manual.html#affine-transformations
  793. :param angle_x: the angle on X axis for skewing
  794. :param angle_y: the angle on Y axis for skewing
  795. :param point: reference point for skewing operation
  796. :return None
  797. """
  798. log.debug("parseGerber.Gerber.skew()")
  799. px, py = point
  800. if angle_x == 0 and angle_y == 0:
  801. return
  802. # variables to display the percentage of work done
  803. self.geo_len = 0
  804. try:
  805. self.geo_len = len(self.solid_geometry)
  806. except TypeError:
  807. self.geo_len = 1
  808. self.old_disp_number = 0
  809. self.el_count = 0
  810. def skew_geom(obj):
  811. if type(obj) is list:
  812. new_obj = []
  813. for g in obj:
  814. new_obj.append(skew_geom(g))
  815. return new_obj
  816. else:
  817. try:
  818. self.el_count += 1
  819. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  820. if self.old_disp_number < disp_number <= 100:
  821. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  822. self.old_disp_number = disp_number
  823. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  824. except AttributeError:
  825. return obj
  826. self.solid_geometry = skew_geom(self.solid_geometry)
  827. self.follow_geometry = skew_geom(self.follow_geometry)
  828. # we need to skew the geometry stored in the Gerber apertures, too
  829. try:
  830. for apid in self.apertures:
  831. if 'geometry' in self.apertures[apid]:
  832. for geo_el in self.apertures[apid]['geometry']:
  833. if 'solid' in geo_el:
  834. geo_el['solid'] = skew_geom(geo_el['solid'])
  835. if 'follow' in geo_el:
  836. geo_el['follow'] = skew_geom(geo_el['follow'])
  837. if 'clear' in geo_el:
  838. geo_el['clear'] = skew_geom(geo_el['clear'])
  839. except Exception as e:
  840. log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
  841. return 'fail'
  842. self.app.inform.emit('[success] %s' % _("Gerber Skew done."))
  843. self.app.proc_container.new_text = ''
  844. def rotate(self, angle, point):
  845. """
  846. Rotate an object by a given angle around given coords (point)
  847. :param angle:
  848. :param point:
  849. :return:
  850. """
  851. log.debug("parseGerber.Gerber.rotate()")
  852. px, py = point
  853. if angle == 0:
  854. return
  855. # variables to display the percentage of work done
  856. self.geo_len = 0
  857. try:
  858. for __ in self.solid_geometry:
  859. self.geo_len += 1
  860. except TypeError:
  861. self.geo_len = 1
  862. self.old_disp_number = 0
  863. self.el_count = 0
  864. def rotate_geom(obj):
  865. if type(obj) is list:
  866. new_obj = []
  867. for g in obj:
  868. new_obj.append(rotate_geom(g))
  869. return new_obj
  870. else:
  871. try:
  872. self.el_count += 1
  873. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  874. if self.old_disp_number < disp_number <= 100:
  875. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  876. self.old_disp_number = disp_number
  877. return affinity.rotate(obj, angle, origin=(px, py))
  878. except AttributeError:
  879. return obj
  880. self.solid_geometry = rotate_geom(self.solid_geometry)
  881. self.follow_geometry = rotate_geom(self.follow_geometry)
  882. # we need to rotate the geometry stored in the Gerber apertures, too
  883. try:
  884. for apid in self.apertures:
  885. if 'geometry' in self.apertures[apid]:
  886. for geo_el in self.apertures[apid]['geometry']:
  887. if 'solid' in geo_el:
  888. geo_el['solid'] = rotate_geom(geo_el['solid'])
  889. if 'follow' in geo_el:
  890. geo_el['follow'] = rotate_geom(geo_el['follow'])
  891. if 'clear' in geo_el:
  892. geo_el['clear'] = rotate_geom(geo_el['clear'])
  893. except Exception as e:
  894. log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
  895. return 'fail'
  896. self.app.inform.emit('[success] %s' %
  897. _("Gerber Rotate done."))
  898. self.app.proc_container.new_text = ''
  899. def parse_number(strnumber):
  900. """
  901. Parse a single number of HPGL2 coordinates.
  902. :param strnumber: String containing a number
  903. from a coordinate data block, possibly with a leading sign.
  904. :type strnumber: str
  905. :return: The number in floating point.
  906. :rtype: float
  907. """
  908. return float(strnumber) / 40.0 # in milimeters