- renamed classes to have shorter names and grouped
This commit is contained in:
429
AppParsers/ParseHPGL2.py
Normal file
429
AppParsers/ParseHPGL2.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# ############################################################
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
from Common import GracefulException as grace
|
||||
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_painttooldia"],
|
||||
"paintmargin": self.app.defaults["tools_paintmargin"],
|
||||
"paintmethod": self.app.defaults["tools_paintmethod"],
|
||||
"selectmethod": self.app.defaults["tools_selectmethod"],
|
||||
"pathconnect": self.app.defaults["tools_pathconnect"],
|
||||
"paintcontour": self.app.defaults["tools_paintcontour"],
|
||||
"paintoverlap": self.app.defaults["tools_paintoverlap"],
|
||||
|
||||
"nccoverlap": self.app.defaults["tools_nccoverlap"],
|
||||
"nccmargin": self.app.defaults["tools_nccmargin"],
|
||||
"nccmethod": self.app.defaults["tools_nccmethod"],
|
||||
"nccconnect": self.app.defaults["tools_nccconnect"],
|
||||
"ncccontour": self.app.defaults["tools_ncccontour"],
|
||||
"nccrest": self.app.defaults["tools_nccrest"]
|
||||
})
|
||||
|
||||
# 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")))
|
||||
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
|
||||
Reference in New Issue
Block a user