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 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 = {}
  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 FlatCAMApp.GracefulException
  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 = 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