ParseHPGL2.py 17 KB

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