flatcam/appParsers/ParseHPGL2.py

449 lines
20 KiB
Python

# ############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# File Author: Marius Adrian Stanciu (c) #
# Date: 12/12/2019 #
# MIT Licence #
# ############################################################
from camlib import arc, three_point_circle, grace
import numpy as np
import re
import logging
import traceback
from copy import deepcopy
import sys
from shapely.ops import unary_union
from shapely.geometry import LineString, Point
# import AppTranslation as fcTranslate
import gettext
import builtins
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class HPGL2:
"""
HPGL2 parsing.
"""
def __init__(self, app):
"""
The constructor takes FlatCAMApp.App as parameter.
"""
self.app = app
# How to approximate a circle with lines.
self.steps_per_circle = int(self.app.defaults["geometry_circle_steps"])
self.decimals = self.app.decimals
# store the file units here
self.units = 'MM'
# storage for the tools
self.tools = {}
self.default_data = {}
self.default_data.update({
"name": '_ncc',
"plot": self.app.defaults["geometry_plot"],
"cutz": self.app.defaults["geometry_cutz"],
"vtipdia": self.app.defaults["geometry_vtipdia"],
"vtipangle": self.app.defaults["geometry_vtipangle"],
"travelz": self.app.defaults["geometry_travelz"],
"feedrate": self.app.defaults["geometry_feedrate"],
"feedrate_z": self.app.defaults["geometry_feedrate_z"],
"feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
"dwell": self.app.defaults["geometry_dwell"],
"dwelltime": self.app.defaults["geometry_dwelltime"],
"multidepth": self.app.defaults["geometry_multidepth"],
"ppname_g": self.app.defaults["geometry_ppname_g"],
"depthperpass": self.app.defaults["geometry_depthperpass"],
"extracut": self.app.defaults["geometry_extracut"],
"extracut_length": self.app.defaults["geometry_extracut_length"],
"toolchange": self.app.defaults["geometry_toolchange"],
"toolchangez": self.app.defaults["geometry_toolchangez"],
"endz": self.app.defaults["geometry_endz"],
"endxy": self.app.defaults["geometry_endxy"],
"area_exclusion": self.app.defaults["geometry_area_exclusion"],
"area_shape": self.app.defaults["geometry_area_shape"],
"area_strategy": self.app.defaults["geometry_area_strategy"],
"area_overz": self.app.defaults["geometry_area_overz"],
"spindlespeed": self.app.defaults["geometry_spindlespeed"],
"toolchangexy": self.app.defaults["geometry_toolchangexy"],
"startz": self.app.defaults["geometry_startz"],
"tooldia": self.app.defaults["tools_paint_tooldia"],
"tools_paint_offset": self.app.defaults["tools_paint_offset"],
"tools_paint_method": self.app.defaults["tools_paint_method"],
"tools_paint_selectmethod": self.app.defaults["tools_paint_selectmethod"],
"tools_paint_connect": self.app.defaults["tools_paint_connect"],
"tools_paint_contour": self.app.defaults["tools_paint_contour"],
"tools_paint_overlap": self.app.defaults["tools_paint_overlap"],
"tools_paint_rest": self.app.defaults["tools_paint_rest"],
"tools_ncc_operation": self.app.defaults["tools_ncc_operation"],
"tools_ncc_margin": self.app.defaults["tools_ncc_margin"],
"tools_ncc_method": self.app.defaults["tools_ncc_method"],
"tools_ncc_connect": self.app.defaults["tools_ncc_connect"],
"tools_ncc_contour": self.app.defaults["tools_ncc_contour"],
"tools_ncc_overlap": self.app.defaults["tools_ncc_overlap"],
"tools_ncc_rest": self.app.defaults["tools_ncc_rest"],
"tools_ncc_ref": self.app.defaults["tools_ncc_ref"],
"tools_ncc_offset_choice": self.app.defaults["tools_ncc_offset_choice"],
"tools_ncc_offset_value": self.app.defaults["tools_ncc_offset_value"],
"tools_ncc_milling_type": self.app.defaults["tools_ncc_milling_type"],
"tools_iso_passes": self.app.defaults["tools_iso_passes"],
"tools_iso_overlap": self.app.defaults["tools_iso_overlap"],
"tools_iso_milling_type": self.app.defaults["tools_iso_milling_type"],
"tools_iso_follow": self.app.defaults["tools_iso_follow"],
"tools_iso_isotype": self.app.defaults["tools_iso_isotype"],
"tools_iso_rest": self.app.defaults["tools_iso_rest"],
"tools_iso_combine_passes": self.app.defaults["tools_iso_combine_passes"],
"tools_iso_isoexcept": self.app.defaults["tools_iso_isoexcept"],
"tools_iso_selection": self.app.defaults["tools_iso_selection"],
"tools_iso_poly_ints": self.app.defaults["tools_iso_poly_ints"],
"tools_iso_force": self.app.defaults["tools_iso_force"],
"tools_iso_area_shape": self.app.defaults["tools_iso_area_shape"]
})
# will store the geometry here for compatibility reason
self.solid_geometry = None
self.source_file = ''
# ### Parser patterns ## ##
# comment
self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
# select pen
self.sp_re = re.compile(r'SP(\d);?$')
# pen position
self.pen_re = re.compile(r"^(P[U|D]);?$")
# Initialize
self.initialize_re = re.compile(r'^(IN);?$')
# Absolute linear interpolation
self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
# Relative linear interpolation
self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.?\d*),?\s*(-?\d+\.?\d*)*;?$")
# Circular interpolation with radius
self.circ_re = re.compile(r"^CI\s*(\+?\d+\.?\d+?)?\s*;?\s*$")
# Arc interpolation with radius
self.arc_re = re.compile(r"^AA\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
# Arc interpolation with 3 points
self.arc_3pt_re = re.compile(r"^AT\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
self.init_done = None
def parse_file(self, filename):
"""
Creates a list of lines from the HPGL2 file and send it to the main parser.
:param filename: HPGL2 file to parse.
:type filename: str
:return: None
"""
with open(filename, 'r') as gfile:
glines = [line.rstrip('\n') for line in gfile]
self.parse_lines(glines=glines)
def parse_lines(self, glines):
"""
Main HPGL2 parser.
:param glines: HPGL2 code as list of strings, each element being
one line of the source file.
:type glines: list
:return: None
:rtype: None
"""
# Coordinates of the current path, each is [x, y]
path = []
geo_buffer = []
# Current coordinates
current_x = None
current_y = None
# Found coordinates
linear_x = None
linear_y = None
# store the pen (tool) status
pen_status = 'up'
# store the current tool here
current_tool = None
# ### Parsing starts here ## ##
line_num = 0
gline = ""
self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("Lines").lower()))
try:
for gline in glines:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
line_num += 1
self.source_file += gline + '\n'
# Cleanup #
gline = gline.strip(' \r\n')
# log.debug("Line=%3s %s" % (line_num, gline))
# ###################
# Ignored lines #####
# Comments #####
# ###################
match = self.comment_re.search(gline)
if match:
log.debug(str(match.group(1)))
continue
# search for the initialization
match = self.initialize_re.search(gline)
if match:
self.init_done = True
continue
if self.init_done is True:
# tools detection
match = self.sp_re.search(gline)
if match:
tool = match.group(1)
# self.tools[tool] = {}
self.tools.update({
tool: {
'tooldia': float('%.*f' %
(
self.decimals,
float(self.app.defaults['geometry_cnctooldia'])
)
),
'offset': 'Path',
'offset_value': 0.0,
'type': 'Iso',
'tool_type': 'C1',
'data': deepcopy(self.default_data),
'solid_geometry': list()
}
})
if current_tool:
if path:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
current_tool = tool
continue
# pen status detection
match = self.pen_re.search(gline)
if match:
pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
continue
# Linear interpolation
match = self.abs_move_re.search(gline)
if match:
# Parse coordinates
if match.group(1) is not None:
linear_x = parse_number(match.group(1))
current_x = linear_x
else:
linear_x = current_x
if match.group(2) is not None:
linear_y = parse_number(match.group(2))
current_y = linear_y
else:
linear_y = current_y
# Pen down: add segment
if pen_status == 'down':
# if linear_x or linear_y are None, ignore those
if current_x is not None and current_y is not None:
# only add the point if it's a new one otherwise skip it (harder to process)
if path[-1] != [current_x, current_y]:
path.append([current_x, current_y])
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
elif pen_status == 'up':
if len(path) > 1:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
# if linear_x or linear_y are None, ignore those
if linear_x is not None and linear_y is not None:
path = [[linear_x, linear_y]] # Start new path
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
# log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
continue
# Circular interpolation
match = self.circ_re.search(gline)
if match:
if len(path) > 1:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
# if linear_x or linear_y are None, ignore those
if linear_x is not None and linear_y is not None:
path = [[linear_x, linear_y]] # Start new path
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
if current_x is not None and current_y is not None:
radius = float(match.group(1))
geo = Point((current_x, current_y)).buffer(radius, int(self.steps_per_circle))
geo_line = geo.exterior
self.tools[current_tool]['solid_geometry'].append(geo_line)
geo_buffer.append(geo_line)
continue
# Arc interpolation with radius
match = self.arc_re.search(gline)
if match:
if len(path) > 1:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
# if linear_x or linear_y are None, ignore those
if linear_x is not None and linear_y is not None:
path = [[linear_x, linear_y]] # Start new path
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
if current_x is not None and current_y is not None:
center = [parse_number(match.group(1)), parse_number(match.group(2))]
angle = np.deg2rad(float(match.group(3)))
p1 = [current_x, current_y]
arcdir = "ccw" if angle >= 0.0 else "cw"
radius = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = startangle + angle
geo = LineString(arc(center, radius, startangle, stopangle, arcdir, self.steps_per_circle))
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
line_coords = list(geo.coords)
current_x = line_coords[0]
current_y = line_coords[1]
continue
# Arc interpolation with 3 points
match = self.arc_3pt_re.search(gline)
if match:
if len(path) > 1:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
# if linear_x or linear_y are None, ignore those
if linear_x is not None and linear_y is not None:
path = [[linear_x, linear_y]] # Start new path
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
if current_x is not None and current_y is not None:
p1 = [current_x, current_y]
p3 = [parse_number(match.group(1)), parse_number(match.group(2))]
p2 = [parse_number(match.group(3)), parse_number(match.group(4))]
try:
center, radius, t = three_point_circle(p1, p2, p3)
except TypeError:
return
direction = 'cw' if np.sign(t) > 0 else 'ccw'
startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
geo = LineString(arc(center, radius, startangle, stopangle,
direction, self.steps_per_circle))
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
# p2 is the end point for the 3-pt circle
current_x = p2[0]
current_y = p2[1]
continue
# ## Line did not match any pattern. Warn user.
log.warning("Line ignored (%d): %s" % (line_num, gline))
if not geo_buffer and not self.solid_geometry:
log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
return 'fail'
log.warning("Joining %d polygons." % len(geo_buffer))
self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
new_poly = unary_union(geo_buffer)
self.solid_geometry = new_poly
except Exception as err:
ex_type, ex, tb = sys.exc_info()
traceback.print_tb(tb)
print(traceback.format_exc())
log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
def parse_number(strnumber):
"""
Parse a single number of HPGL2 coordinates.
:param strnumber: String containing a number
from a coordinate data block, possibly with a leading sign.
:type strnumber: str
:return: The number in floating point.
:rtype: float
"""
return float(strnumber) / 40.0 # in milimeters