ParseHPGL2.py 17 KB

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