- removed the "follow" functionality from the Isolation Tool - created a new application Tool named Follow Tool - added the "follow" functionality in the Follow Tool and added the new feature of allowing to perform "follow" on an area selection
448 lines
20 KiB
Python
448 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_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
|