ParseHPGL2.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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 Common import GracefulException as grace
  18. import AppTranslation 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. "area_exclusion": self.app.defaults["geometry_area_exclusion"],
  63. "area_shape": self.app.defaults["geometry_area_shape"],
  64. "area_strategy": self.app.defaults["geometry_area_strategy"],
  65. "area_overz": self.app.defaults["geometry_area_overz"],
  66. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  67. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  68. "startz": self.app.defaults["geometry_startz"],
  69. "tooldia": self.app.defaults["tools_painttooldia"],
  70. "paintmargin": self.app.defaults["tools_paintmargin"],
  71. "paintmethod": self.app.defaults["tools_paintmethod"],
  72. "selectmethod": self.app.defaults["tools_selectmethod"],
  73. "pathconnect": self.app.defaults["tools_pathconnect"],
  74. "paintcontour": self.app.defaults["tools_paintcontour"],
  75. "paintoverlap": self.app.defaults["tools_paintoverlap"],
  76. "nccoverlap": self.app.defaults["tools_nccoverlap"],
  77. "nccmargin": self.app.defaults["tools_nccmargin"],
  78. "nccmethod": self.app.defaults["tools_nccmethod"],
  79. "nccconnect": self.app.defaults["tools_nccconnect"],
  80. "ncccontour": self.app.defaults["tools_ncccontour"],
  81. "nccrest": self.app.defaults["tools_nccrest"]
  82. })
  83. # will store the geometry here for compatibility reason
  84. self.solid_geometry = None
  85. self.source_file = ''
  86. # ### Parser patterns ## ##
  87. # comment
  88. self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
  89. # select pen
  90. self.sp_re = re.compile(r'SP(\d);?$')
  91. # pen position
  92. self.pen_re = re.compile(r"^(P[U|D]);?$")
  93. # Initialize
  94. self.initialize_re = re.compile(r'^(IN);?$')
  95. # Absolute linear interpolation
  96. self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
  97. # Relative linear interpolation
  98. self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
  99. # Circular interpolation with radius
  100. self.circ_re = re.compile(r"^CI\s*(\+?\d+\.?\d+?)?\s*;?\s*$")
  101. # Arc interpolation with radius
  102. self.arc_re = re.compile(r"^AA\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
  103. # Arc interpolation with 3 points
  104. self.arc_3pt_re = re.compile(r"^AT\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
  105. self.init_done = None
  106. def parse_file(self, filename):
  107. """
  108. Creates a list of lines from the HPGL2 file and send it to the main parser.
  109. :param filename: HPGL2 file to parse.
  110. :type filename: str
  111. :return: None
  112. """
  113. with open(filename, 'r') as gfile:
  114. glines = [line.rstrip('\n') for line in gfile]
  115. self.parse_lines(glines=glines)
  116. def parse_lines(self, glines):
  117. """
  118. Main HPGL2 parser.
  119. :param glines: HPGL2 code as list of strings, each element being
  120. one line of the source file.
  121. :type glines: list
  122. :return: None
  123. :rtype: None
  124. """
  125. # Coordinates of the current path, each is [x, y]
  126. path = []
  127. geo_buffer = []
  128. # Current coordinates
  129. current_x = None
  130. current_y = None
  131. # Found coordinates
  132. linear_x = None
  133. linear_y = None
  134. # store the pen (tool) status
  135. pen_status = 'up'
  136. # store the current tool here
  137. current_tool = None
  138. # ### Parsing starts here ## ##
  139. line_num = 0
  140. gline = ""
  141. self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
  142. try:
  143. for gline in glines:
  144. if self.app.abort_flag:
  145. # graceful abort requested by the user
  146. raise grace
  147. line_num += 1
  148. self.source_file += gline + '\n'
  149. # Cleanup #
  150. gline = gline.strip(' \r\n')
  151. # log.debug("Line=%3s %s" % (line_num, gline))
  152. # ###################
  153. # Ignored lines #####
  154. # Comments #####
  155. # ###################
  156. match = self.comment_re.search(gline)
  157. if match:
  158. log.debug(str(match.group(1)))
  159. continue
  160. # search for the initialization
  161. match = self.initialize_re.search(gline)
  162. if match:
  163. self.init_done = True
  164. continue
  165. if self.init_done is True:
  166. # tools detection
  167. match = self.sp_re.search(gline)
  168. if match:
  169. tool = match.group(1)
  170. # self.tools[tool] = {}
  171. self.tools.update({
  172. tool: {
  173. 'tooldia': float('%.*f' %
  174. (
  175. self.decimals,
  176. float(self.app.defaults['geometry_cnctooldia'])
  177. )
  178. ),
  179. 'offset': 'Path',
  180. 'offset_value': 0.0,
  181. 'type': 'Iso',
  182. 'tool_type': 'C1',
  183. 'data': deepcopy(self.default_data),
  184. 'solid_geometry': list()
  185. }
  186. })
  187. if current_tool:
  188. if path:
  189. geo = LineString(path)
  190. self.tools[current_tool]['solid_geometry'].append(geo)
  191. geo_buffer.append(geo)
  192. path[:] = []
  193. current_tool = tool
  194. continue
  195. # pen status detection
  196. match = self.pen_re.search(gline)
  197. if match:
  198. pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
  199. continue
  200. # Linear interpolation
  201. match = self.abs_move_re.search(gline)
  202. if match:
  203. # Parse coordinates
  204. if match.group(1) is not None:
  205. linear_x = parse_number(match.group(1))
  206. current_x = linear_x
  207. else:
  208. linear_x = current_x
  209. if match.group(2) is not None:
  210. linear_y = parse_number(match.group(2))
  211. current_y = linear_y
  212. else:
  213. linear_y = current_y
  214. # Pen down: add segment
  215. if pen_status == 'down':
  216. # if linear_x or linear_y are None, ignore those
  217. if current_x is not None and current_y is not None:
  218. # only add the point if it's a new one otherwise skip it (harder to process)
  219. if path[-1] != [current_x, current_y]:
  220. path.append([current_x, current_y])
  221. else:
  222. self.app.inform.emit('[WARNING] %s: %s' %
  223. (_("Coordinates missing, line ignored"), str(gline)))
  224. elif pen_status == 'up':
  225. if len(path) > 1:
  226. geo = LineString(path)
  227. self.tools[current_tool]['solid_geometry'].append(geo)
  228. geo_buffer.append(geo)
  229. path[:] = []
  230. # if linear_x or linear_y are None, ignore those
  231. if linear_x is not None and linear_y is not None:
  232. path = [[linear_x, linear_y]] # Start new path
  233. else:
  234. self.app.inform.emit('[WARNING] %s: %s' %
  235. (_("Coordinates missing, line ignored"), str(gline)))
  236. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  237. continue
  238. # Circular interpolation
  239. match = self.circ_re.search(gline)
  240. if match:
  241. if len(path) > 1:
  242. geo = LineString(path)
  243. self.tools[current_tool]['solid_geometry'].append(geo)
  244. geo_buffer.append(geo)
  245. path[:] = []
  246. # if linear_x or linear_y are None, ignore those
  247. if linear_x is not None and linear_y is not None:
  248. path = [[linear_x, linear_y]] # Start new path
  249. else:
  250. self.app.inform.emit('[WARNING] %s: %s' %
  251. (_("Coordinates missing, line ignored"), str(gline)))
  252. if current_x is not None and current_y is not None:
  253. radius = float(match.group(1))
  254. geo = Point((current_x, current_y)).buffer(radius, int(self.steps_per_circle))
  255. geo_line = geo.exterior
  256. self.tools[current_tool]['solid_geometry'].append(geo_line)
  257. geo_buffer.append(geo_line)
  258. continue
  259. # Arc interpolation with radius
  260. match = self.arc_re.search(gline)
  261. if match:
  262. if len(path) > 1:
  263. geo = LineString(path)
  264. self.tools[current_tool]['solid_geometry'].append(geo)
  265. geo_buffer.append(geo)
  266. path[:] = []
  267. # if linear_x or linear_y are None, ignore those
  268. if linear_x is not None and linear_y is not None:
  269. path = [[linear_x, linear_y]] # Start new path
  270. else:
  271. self.app.inform.emit('[WARNING] %s: %s' %
  272. (_("Coordinates missing, line ignored"), str(gline)))
  273. if current_x is not None and current_y is not None:
  274. center = [parse_number(match.group(1)), parse_number(match.group(2))]
  275. angle = np.deg2rad(float(match.group(3)))
  276. p1 = [current_x, current_y]
  277. arcdir = "ccw" if angle >= 0.0 else "cw"
  278. radius = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
  279. startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
  280. stopangle = startangle + angle
  281. geo = LineString(arc(center, radius, startangle, stopangle, arcdir, self.steps_per_circle))
  282. self.tools[current_tool]['solid_geometry'].append(geo)
  283. geo_buffer.append(geo)
  284. line_coords = list(geo.coords)
  285. current_x = line_coords[0]
  286. current_y = line_coords[1]
  287. continue
  288. # Arc interpolation with 3 points
  289. match = self.arc_3pt_re.search(gline)
  290. if match:
  291. if len(path) > 1:
  292. geo = LineString(path)
  293. self.tools[current_tool]['solid_geometry'].append(geo)
  294. geo_buffer.append(geo)
  295. path[:] = []
  296. # if linear_x or linear_y are None, ignore those
  297. if linear_x is not None and linear_y is not None:
  298. path = [[linear_x, linear_y]] # Start new path
  299. else:
  300. self.app.inform.emit('[WARNING] %s: %s' %
  301. (_("Coordinates missing, line ignored"), str(gline)))
  302. if current_x is not None and current_y is not None:
  303. p1 = [current_x, current_y]
  304. p3 = [parse_number(match.group(1)), parse_number(match.group(2))]
  305. p2 = [parse_number(match.group(3)), parse_number(match.group(4))]
  306. try:
  307. center, radius, t = three_point_circle(p1, p2, p3)
  308. except TypeError:
  309. return
  310. direction = 'cw' if np.sign(t) > 0 else 'ccw'
  311. startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
  312. stopangle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
  313. geo = LineString(arc(center, radius, startangle, stopangle,
  314. direction, self.steps_per_circle))
  315. self.tools[current_tool]['solid_geometry'].append(geo)
  316. geo_buffer.append(geo)
  317. # p2 is the end point for the 3-pt circle
  318. current_x = p2[0]
  319. current_y = p2[1]
  320. continue
  321. # ## Line did not match any pattern. Warn user.
  322. log.warning("Line ignored (%d): %s" % (line_num, gline))
  323. if not geo_buffer and not self.solid_geometry:
  324. log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
  325. return 'fail'
  326. log.warning("Joining %d polygons." % len(geo_buffer))
  327. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
  328. new_poly = unary_union(geo_buffer)
  329. self.solid_geometry = new_poly
  330. except Exception as err:
  331. ex_type, ex, tb = sys.exc_info()
  332. traceback.print_tb(tb)
  333. print(traceback.format_exc())
  334. log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
  335. loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
  336. self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
  337. def parse_number(strnumber):
  338. """
  339. Parse a single number of HPGL2 coordinates.
  340. :param strnumber: String containing a number
  341. from a coordinate data block, possibly with a leading sign.
  342. :type strnumber: str
  343. :return: The number in floating point.
  344. :rtype: float
  345. """
  346. return float(strnumber) / 40.0 # in milimeters