ParseHPGL2.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 12/12/2019 #
  6. # MIT Licence #
  7. # ############################################################
  8. from camlib import arc, three_point_circle, grace
  9. import numpy as np
  10. import re
  11. import logging
  12. import traceback
  13. from copy import deepcopy
  14. import sys
  15. from shapely.ops import unary_union
  16. from shapely.geometry import LineString, Point
  17. # import AppTranslation as fcTranslate
  18. import gettext
  19. import builtins
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. log = logging.getLogger('base')
  23. class HPGL2:
  24. """
  25. HPGL2 parsing.
  26. """
  27. def __init__(self, app):
  28. """
  29. The constructor takes FlatCAMApp.App as parameter.
  30. """
  31. self.app = app
  32. # How to approximate a circle with lines.
  33. self.steps_per_circle = int(self.app.defaults["geometry_circle_steps"])
  34. self.decimals = self.app.decimals
  35. # store the file units here
  36. self.units = 'MM'
  37. # storage for the tools
  38. self.tools = {}
  39. self.default_data = {}
  40. self.default_data.update({
  41. "name": '_ncc',
  42. "plot": self.app.defaults["geometry_plot"],
  43. "cutz": self.app.defaults["geometry_cutz"],
  44. "vtipdia": self.app.defaults["geometry_vtipdia"],
  45. "vtipangle": self.app.defaults["geometry_vtipangle"],
  46. "travelz": self.app.defaults["geometry_travelz"],
  47. "feedrate": self.app.defaults["geometry_feedrate"],
  48. "feedrate_z": self.app.defaults["geometry_feedrate_z"],
  49. "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
  50. "dwell": self.app.defaults["geometry_dwell"],
  51. "dwelltime": self.app.defaults["geometry_dwelltime"],
  52. "multidepth": self.app.defaults["geometry_multidepth"],
  53. "ppname_g": self.app.defaults["geometry_ppname_g"],
  54. "depthperpass": self.app.defaults["geometry_depthperpass"],
  55. "extracut": self.app.defaults["geometry_extracut"],
  56. "extracut_length": self.app.defaults["geometry_extracut_length"],
  57. "toolchange": self.app.defaults["geometry_toolchange"],
  58. "toolchangez": self.app.defaults["geometry_toolchangez"],
  59. "endz": self.app.defaults["geometry_endz"],
  60. "endxy": self.app.defaults["geometry_endxy"],
  61. "area_exclusion": self.app.defaults["geometry_area_exclusion"],
  62. "area_shape": self.app.defaults["geometry_area_shape"],
  63. "area_strategy": self.app.defaults["geometry_area_strategy"],
  64. "area_overz": self.app.defaults["geometry_area_overz"],
  65. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  66. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  67. "startz": self.app.defaults["geometry_startz"],
  68. "tooldia": self.app.defaults["tools_paint_tooldia"],
  69. "tools_paint_offset": self.app.defaults["tools_paint_offset"],
  70. "tools_paint_method": self.app.defaults["tools_paint_method"],
  71. "tools_paint_selectmethod": self.app.defaults["tools_paint_selectmethod"],
  72. "tools_paint_connect": self.app.defaults["tools_paint_connect"],
  73. "tools_paint_contour": self.app.defaults["tools_paint_contour"],
  74. "tools_paint_overlap": self.app.defaults["tools_paint_overlap"],
  75. "tools_paint_rest": self.app.defaults["tools_paint_rest"],
  76. "tools_ncc_operation": self.app.defaults["tools_ncc_operation"],
  77. "tools_ncc_margin": self.app.defaults["tools_ncc_margin"],
  78. "tools_ncc_method": self.app.defaults["tools_ncc_method"],
  79. "tools_ncc_connect": self.app.defaults["tools_ncc_connect"],
  80. "tools_ncc_contour": self.app.defaults["tools_ncc_contour"],
  81. "tools_ncc_overlap": self.app.defaults["tools_ncc_overlap"],
  82. "tools_ncc_rest": self.app.defaults["tools_ncc_rest"],
  83. "tools_ncc_ref": self.app.defaults["tools_ncc_ref"],
  84. "tools_ncc_offset_choice": self.app.defaults["tools_ncc_offset_choice"],
  85. "tools_ncc_offset_value": self.app.defaults["tools_ncc_offset_value"],
  86. "tools_ncc_milling_type": self.app.defaults["tools_ncc_milling_type"],
  87. "tools_iso_passes": self.app.defaults["tools_iso_passes"],
  88. "tools_iso_overlap": self.app.defaults["tools_iso_overlap"],
  89. "tools_iso_milling_type": self.app.defaults["tools_iso_milling_type"],
  90. "tools_iso_follow": self.app.defaults["tools_iso_follow"],
  91. "tools_iso_isotype": self.app.defaults["tools_iso_isotype"],
  92. "tools_iso_rest": self.app.defaults["tools_iso_rest"],
  93. "tools_iso_combine_passes": self.app.defaults["tools_iso_combine_passes"],
  94. "tools_iso_isoexcept": self.app.defaults["tools_iso_isoexcept"],
  95. "tools_iso_selection": self.app.defaults["tools_iso_selection"],
  96. "tools_iso_poly_ints": self.app.defaults["tools_iso_poly_ints"],
  97. "tools_iso_force": self.app.defaults["tools_iso_force"],
  98. "tools_iso_area_shape": self.app.defaults["tools_iso_area_shape"]
  99. })
  100. # will store the geometry here for compatibility reason
  101. self.solid_geometry = None
  102. self.source_file = ''
  103. # ### Parser patterns ## ##
  104. # comment
  105. self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
  106. # select pen
  107. self.sp_re = re.compile(r'SP(\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. # Absolute linear interpolation
  113. self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
  114. # Relative linear interpolation
  115. self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
  116. # Circular interpolation with radius
  117. self.circ_re = re.compile(r"^CI\s*(\+?\d+\.?\d+?)?\s*;?\s*$")
  118. # Arc interpolation with radius
  119. self.arc_re = re.compile(r"^AA\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
  120. # Arc interpolation with 3 points
  121. self.arc_3pt_re = re.compile(r"^AT\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
  122. self.init_done = None
  123. def parse_file(self, filename):
  124. """
  125. Creates a list of lines from the HPGL2 file and send it to the main parser.
  126. :param filename: HPGL2 file to parse.
  127. :type filename: str
  128. :return: None
  129. """
  130. with open(filename, 'r') as gfile:
  131. glines = [line.rstrip('\n') for line in gfile]
  132. self.parse_lines(glines=glines)
  133. def parse_lines(self, glines):
  134. """
  135. Main HPGL2 parser.
  136. :param glines: HPGL2 code as list of strings, each element being
  137. one line of the source file.
  138. :type glines: list
  139. :return: None
  140. :rtype: None
  141. """
  142. # Coordinates of the current path, each is [x, y]
  143. path = []
  144. geo_buffer = []
  145. # Current coordinates
  146. current_x = None
  147. current_y = None
  148. # Found coordinates
  149. linear_x = None
  150. linear_y = None
  151. # store the pen (tool) status
  152. pen_status = 'up'
  153. # store the current tool here
  154. current_tool = None
  155. # ### Parsing starts here ## ##
  156. line_num = 0
  157. gline = ""
  158. self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("Lines").lower()))
  159. try:
  160. for gline in glines:
  161. if self.app.abort_flag:
  162. # graceful abort requested by the user
  163. raise grace
  164. line_num += 1
  165. self.source_file += gline + '\n'
  166. # Cleanup #
  167. gline = gline.strip(' \r\n')
  168. # log.debug("Line=%3s %s" % (line_num, gline))
  169. # ###################
  170. # Ignored lines #####
  171. # Comments #####
  172. # ###################
  173. match = self.comment_re.search(gline)
  174. if match:
  175. log.debug(str(match.group(1)))
  176. continue
  177. # search for the initialization
  178. match = self.initialize_re.search(gline)
  179. if match:
  180. self.init_done = True
  181. continue
  182. if self.init_done is True:
  183. # tools detection
  184. match = self.sp_re.search(gline)
  185. if match:
  186. tool = match.group(1)
  187. # self.tools[tool] = {}
  188. self.tools.update({
  189. tool: {
  190. 'tooldia': float('%.*f' %
  191. (
  192. self.decimals,
  193. float(self.app.defaults['geometry_cnctooldia'])
  194. )
  195. ),
  196. 'offset': 'Path',
  197. 'offset_value': 0.0,
  198. 'type': 'Iso',
  199. 'tool_type': 'C1',
  200. 'data': deepcopy(self.default_data),
  201. 'solid_geometry': list()
  202. }
  203. })
  204. if current_tool:
  205. if path:
  206. geo = LineString(path)
  207. self.tools[current_tool]['solid_geometry'].append(geo)
  208. geo_buffer.append(geo)
  209. path[:] = []
  210. current_tool = tool
  211. continue
  212. # pen status detection
  213. match = self.pen_re.search(gline)
  214. if match:
  215. pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
  216. continue
  217. # Linear interpolation
  218. match = self.abs_move_re.search(gline)
  219. if match:
  220. # Parse coordinates
  221. if match.group(1) is not None:
  222. linear_x = parse_number(match.group(1))
  223. current_x = linear_x
  224. else:
  225. linear_x = current_x
  226. if match.group(2) is not None:
  227. linear_y = parse_number(match.group(2))
  228. current_y = linear_y
  229. else:
  230. linear_y = current_y
  231. # Pen down: add segment
  232. if pen_status == 'down':
  233. # if linear_x or linear_y are None, ignore those
  234. if current_x is not None and current_y is not None:
  235. # only add the point if it's a new one otherwise skip it (harder to process)
  236. if path[-1] != [current_x, current_y]:
  237. path.append([current_x, current_y])
  238. else:
  239. self.app.inform.emit('[WARNING] %s: %s' %
  240. (_("Coordinates missing, line ignored"), str(gline)))
  241. elif pen_status == 'up':
  242. if len(path) > 1:
  243. geo = LineString(path)
  244. self.tools[current_tool]['solid_geometry'].append(geo)
  245. geo_buffer.append(geo)
  246. path[:] = []
  247. # if linear_x or linear_y are None, ignore those
  248. if linear_x is not None and linear_y is not None:
  249. path = [[linear_x, linear_y]] # Start new path
  250. else:
  251. self.app.inform.emit('[WARNING] %s: %s' %
  252. (_("Coordinates missing, line ignored"), str(gline)))
  253. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  254. continue
  255. # Circular interpolation
  256. match = self.circ_re.search(gline)
  257. if match:
  258. if len(path) > 1:
  259. geo = LineString(path)
  260. self.tools[current_tool]['solid_geometry'].append(geo)
  261. geo_buffer.append(geo)
  262. path[:] = []
  263. # if linear_x or linear_y are None, ignore those
  264. if linear_x is not None and linear_y is not None:
  265. path = [[linear_x, linear_y]] # Start new path
  266. else:
  267. self.app.inform.emit('[WARNING] %s: %s' %
  268. (_("Coordinates missing, line ignored"), str(gline)))
  269. if current_x is not None and current_y is not None:
  270. radius = float(match.group(1))
  271. geo = Point((current_x, current_y)).buffer(radius, int(self.steps_per_circle))
  272. geo_line = geo.exterior
  273. self.tools[current_tool]['solid_geometry'].append(geo_line)
  274. geo_buffer.append(geo_line)
  275. continue
  276. # Arc interpolation with radius
  277. match = self.arc_re.search(gline)
  278. if match:
  279. if len(path) > 1:
  280. geo = LineString(path)
  281. self.tools[current_tool]['solid_geometry'].append(geo)
  282. geo_buffer.append(geo)
  283. path[:] = []
  284. # if linear_x or linear_y are None, ignore those
  285. if linear_x is not None and linear_y is not None:
  286. path = [[linear_x, linear_y]] # Start new path
  287. else:
  288. self.app.inform.emit('[WARNING] %s: %s' %
  289. (_("Coordinates missing, line ignored"), str(gline)))
  290. if current_x is not None and current_y is not None:
  291. center = [parse_number(match.group(1)), parse_number(match.group(2))]
  292. angle = np.deg2rad(float(match.group(3)))
  293. p1 = [current_x, current_y]
  294. arcdir = "ccw" if angle >= 0.0 else "cw"
  295. radius = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
  296. startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
  297. stopangle = startangle + angle
  298. geo = LineString(arc(center, radius, startangle, stopangle, arcdir, self.steps_per_circle))
  299. self.tools[current_tool]['solid_geometry'].append(geo)
  300. geo_buffer.append(geo)
  301. line_coords = list(geo.coords)
  302. current_x = line_coords[0]
  303. current_y = line_coords[1]
  304. continue
  305. # Arc interpolation with 3 points
  306. match = self.arc_3pt_re.search(gline)
  307. if match:
  308. if len(path) > 1:
  309. geo = LineString(path)
  310. self.tools[current_tool]['solid_geometry'].append(geo)
  311. geo_buffer.append(geo)
  312. path[:] = []
  313. # if linear_x or linear_y are None, ignore those
  314. if linear_x is not None and linear_y is not None:
  315. path = [[linear_x, linear_y]] # Start new path
  316. else:
  317. self.app.inform.emit('[WARNING] %s: %s' %
  318. (_("Coordinates missing, line ignored"), str(gline)))
  319. if current_x is not None and current_y is not None:
  320. p1 = [current_x, current_y]
  321. p3 = [parse_number(match.group(1)), parse_number(match.group(2))]
  322. p2 = [parse_number(match.group(3)), parse_number(match.group(4))]
  323. try:
  324. center, radius, t = three_point_circle(p1, p2, p3)
  325. except TypeError:
  326. return
  327. direction = 'cw' if np.sign(t) > 0 else 'ccw'
  328. startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
  329. stopangle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
  330. geo = LineString(arc(center, radius, startangle, stopangle,
  331. direction, self.steps_per_circle))
  332. self.tools[current_tool]['solid_geometry'].append(geo)
  333. geo_buffer.append(geo)
  334. # p2 is the end point for the 3-pt circle
  335. current_x = p2[0]
  336. current_y = p2[1]
  337. continue
  338. # ## Line did not match any pattern. Warn user.
  339. log.warning("Line ignored (%d): %s" % (line_num, gline))
  340. if not geo_buffer and not self.solid_geometry:
  341. log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
  342. return 'fail'
  343. log.warning("Joining %d polygons." % len(geo_buffer))
  344. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
  345. new_poly = unary_union(geo_buffer)
  346. self.solid_geometry = new_poly
  347. except Exception as err:
  348. ex_type, ex, tb = sys.exc_info()
  349. traceback.print_tb(tb)
  350. print(traceback.format_exc())
  351. log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
  352. loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
  353. self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
  354. def parse_number(strnumber):
  355. """
  356. Parse a single number of HPGL2 coordinates.
  357. :param strnumber: String containing a number
  358. from a coordinate data block, possibly with a leading sign.
  359. :type strnumber: str
  360. :return: The number in floating point.
  361. :rtype: float
  362. """
  363. return float(strnumber) / 40.0 # in milimeters