- finished a very rough and limited HPGL2 file import

This commit is contained in:
Marius Stanciu 2019-12-12 21:29:38 +02:00 committed by Marius
parent 0c057574c9
commit 02b567971d
5 changed files with 423 additions and 498 deletions

View File

@ -63,6 +63,8 @@ from flatcamEditors.FlatCAMExcEditor import FlatCAMExcEditor
from flatcamEditors.FlatCAMGrbEditor import FlatCAMGrbEditor from flatcamEditors.FlatCAMGrbEditor import FlatCAMGrbEditor
from flatcamEditors.FlatCAMTextEditor import TextEditor from flatcamEditors.FlatCAMTextEditor import TextEditor
from flatcamParsers.ParseHPGL2 import HPGL2
from FlatCAMProcess import * from FlatCAMProcess import *
from FlatCAMWorkerStack import WorkerStack from FlatCAMWorkerStack import WorkerStack
# from flatcamGUI.VisPyVisuals import Color # from flatcamGUI.VisPyVisuals import Color
@ -1771,7 +1773,7 @@ class App(QtCore.QObject):
self.ui.menufileimportdxf.triggered.connect(lambda: self.on_file_importdxf("geometry")) self.ui.menufileimportdxf.triggered.connect(lambda: self.on_file_importdxf("geometry"))
self.ui.menufileimportdxf_as_gerber.triggered.connect(lambda: self.on_file_importdxf("gerber")) self.ui.menufileimportdxf_as_gerber.triggered.connect(lambda: self.on_file_importdxf("gerber"))
self.ui.menufileimport_hpgl2_as_geo.triggered.connect(self.on_fileopenhpgl2)
self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg) self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng) self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon) self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon)
@ -9295,6 +9297,44 @@ class App(QtCore.QObject):
# thread safe. The new_project() # thread safe. The new_project()
self.open_project(filename) self.open_project(filename)
def on_fileopenhpgl2(self, signal: bool = None, name=None):
"""
File menu callback for opening a HPGL2.
:param signal: required because clicking the entry will generate a checked signal which needs a container
:return: None
"""
self.report_usage("on_fileopenhpgl2")
App.log.debug("on_fileopenhpgl2()")
_filter_ = "HPGL2 Files (*.plt);;" \
"All Files (*.*)"
if name is None:
try:
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"),
directory=self.get_last_folder(),
filter=_filter_)
except TypeError:
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"), filter=_filter_)
filenames = [str(filename) for filename in filenames]
else:
filenames = [name]
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
"Canvas initialization finished in"), '%.2f' % self.used_time,
_("Opening HPGL2 file.")),
alignment=Qt.AlignBottom | Qt.AlignLeft,
color=QtGui.QColor("gray"))
if len(filenames) == 0:
self.inform.emit('[WARNING_NOTCL] %s' % _("Open HPGL2 file cancelled."))
else:
for filename in filenames:
if filename != '':
self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
def on_file_openconfig(self, signal: bool = None): def on_file_openconfig(self, signal: bool = None):
""" """
File menu callback for opening a config file. File menu callback for opening a config file.
@ -10931,6 +10971,70 @@ class App(QtCore.QObject):
self.inform.emit('[success] %s: %s' % self.inform.emit('[success] %s: %s' %
(_("Opened"), filename)) (_("Opened"), filename))
def open_hpgl2(self, filename, outname=None):
"""
Opens a HPGL2 file, parses it and creates a new object for
it in the program. Thread-safe.
:param outname: Name of the resulting object. None causes the
name to be that of the file.
:param filename: HPGL2 file filename
:type filename: str
:return: None
"""
filename = filename
# How the object should be initialized
def obj_init(geo_obj, app_obj):
# assert isinstance(geo_obj, FlatCAMGeometry), \
# "Expected to initialize a FlatCAMGeometry but got %s" % type(geo_obj)
# Opening the file happens here
obj = HPGL2()
try:
HPGL2.parse_file(obj, filename)
except IOError:
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
return "fail"
except ParseError as err:
app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(err)))
app_obj.log.error(str(err))
return "fail"
except Exception as e:
log.debug("App.open_hpgl2() --> %s" % str(e))
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
msg += traceback.format_exc()
app_obj.inform.emit(msg)
return "fail"
geo_obj.multigeo = True
geo_obj.solid_geometry = obj.solid_geometry
geo_obj.tools = obj.tools
# if geo_obj.is_empty():
# app_obj.inform.emit('[ERROR_NOTCL] %s' %
# _("Object is not HPGL2 file or empty. Aborting object creation."))
# return "fail"
App.log.debug("open_hpgl2()")
with self.proc_container.new(_("Opening HPGL2")) as proc:
# Object name
name = outname or filename.split('/')[-1].split('\\')[-1]
# # ## Object creation # ##
ret = self.new_object("geometry", name, obj_init, autoselected=False)
if ret == 'fail':
self.inform.emit('[ERROR_NOTCL]%s' % _(' Open HPGL2 failed. Probable not a HPGL2 file.'))
return 'fail'
# Register recent file
self.file_opened.emit("geometry", filename)
# GUI feedback
self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
def open_script(self, filename, outname=None, silent=False): def open_script(self, filename, outname=None, silent=False):
""" """
Opens a Script file, parses it and creates a new object for Opens a Script file, parses it and creates a new object for

View File

@ -5744,7 +5744,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
def plot_element(self, element, color='#FF0000FF', visible=None): def plot_element(self, element, color='#FF0000FF', visible=None):
visible = visible if visible else self.options['plot'] visible = visible if visible else self.options['plot']
try: try:
for sub_el in element: for sub_el in element:
self.plot_element(sub_el) self.plot_element(sub_el)

View File

@ -17,6 +17,7 @@ CAD program, and create G-Code for Isolation routing.
- added option to save objects as PDF files in File -> Save menu - added option to save objects as PDF files in File -> Save menu
- optimized the FlatCAMGerber.clear_plot_apertures() method - optimized the FlatCAMGerber.clear_plot_apertures() method
- some changes in the ObjectUI and for the Geometry UI - some changes in the ObjectUI and for the Geometry UI
- finished a very rough and limited HPGL2 file import
11.12.2019 11.12.2019

View File

@ -168,6 +168,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
_('&DXF as Gerber Object ...'), self) _('&DXF as Gerber Object ...'), self)
self.menufileimport.addAction(self.menufileimportdxf_as_gerber) self.menufileimport.addAction(self.menufileimportdxf_as_gerber)
self.menufileimport.addSeparator() self.menufileimport.addSeparator()
self.menufileimport_hpgl2_as_geo = QtWidgets.QAction(QtGui.QIcon('share/dxf16.png'),
_('HPGL2 as Geometry Object ...'), self)
self.menufileimport.addAction(self.menufileimport_hpgl2_as_geo)
self.menufileimport.addSeparator()
# Export ... # Export ...
self.menufileexport = self.menufile.addMenu(QtGui.QIcon('share/export.png'), _('Export')) self.menufileexport = self.menufile.addMenu(QtGui.QIcon('share/export.png'), _('Export'))

View File

@ -1,7 +1,7 @@
# ############################################################ # ############################################################
# FlatCAM: 2D Post-processing for Manufacturing # # FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org # # http://flatcam.org #
# File Author: Marius Adrina Stanciu (c) # # File Author: Marius Adrian Stanciu (c) #
# Date: 12/11/2019 # # Date: 12/11/2019 #
# MIT Licence # # MIT Licence #
# ############################################################ # ############################################################
@ -17,7 +17,7 @@ from copy import deepcopy
import sys import sys
from shapely.ops import cascaded_union, unary_union from shapely.ops import cascaded_union, unary_union
from shapely.geometry import Polygon, MultiPolygon, LineString, Point from shapely.geometry import Polygon, MultiPolygon, LineString, Point, MultiLineString
import shapely.affinity as affinity import shapely.affinity as affinity
from shapely.geometry import box as shply_box from shapely.geometry import box as shply_box
@ -62,7 +62,54 @@ class HPGL2(Geometry):
self.coord_mm_factor = 0.040 self.coord_mm_factor = 0.040
# store the file units here: # store the file units here:
self.units = self.app.defaults['gerber_def_units'] self.units = 'MM'
# storage for the tools
self.tools = dict()
self.default_data = dict()
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"],
"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"]
})
# flag to be set True when tool is detected
self.tool_detected = False
# will store the geometry's as solids # will store the geometry's as solids
self.solid_geometry = None self.solid_geometry = None
@ -82,17 +129,17 @@ class HPGL2(Geometry):
# comment # comment
self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$") self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
# absolute move to x, y # absolute move to x, y
self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$") self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d+?),?\s*(-?\d+\.?\d+?)*;?$")
# relative move to x, y # relative move to x, y
self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$") self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
# pen position # pen position
self.pen_re = re.compile(r"^(P[U|D]);?$") self.pen_re = re.compile(r"^(P[U|D]);?$")
# Initialize # Initialize
self.mode_re = re.compile(r'^(IN);?$') self.initialize_re = re.compile(r'^(IN);?$')
# select pen # select pen
self.sp_re = re.compile(r'SP(\d);?$') self.sp_re = re.compile(r'SP(\d);?$')
self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$') self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$') self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
@ -108,12 +155,6 @@ class HPGL2(Geometry):
self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' + self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' +
'?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$') '?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
# G01/2/3 Occurring without coordinates
self.interp_re = re.compile(r'^(?:G0?([123]))\*')
# Single G74 or multi G75 quadrant for circular interpolation
self.quad_re = re.compile(r'^G7([45]).*\*$')
# Absolute/Relative G90/1 (OBSOLETE) # Absolute/Relative G90/1 (OBSOLETE)
self.absrel_re = re.compile(r'^G9([01])\*$') self.absrel_re = re.compile(r'^G9([01])\*$')
@ -121,25 +162,13 @@ class HPGL2(Geometry):
# in a Gerber file (normal or obsolete ones) # in a Gerber file (normal or obsolete ones)
self.conversion_done = False self.conversion_done = False
self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"] self.in_header = None
def parse_file(self, filename, follow=False): def parse_file(self, filename):
""" """
Calls Gerber.parse_lines() with generator of lines
read from the given file. Will split the lines if multiple
statements are found in a single original line.
The following line is split into two:: :param filename: HPGL2 file to parse.
G54D11*G36*
First is ``G54D11*`` and seconds is ``G36*``.
:param filename: Gerber file to parse.
:type filename: str :type filename: str
:param follow: If true, will not create polygons, just lines
following the gerber path.
:type follow: bool
:return: None :return: None
""" """
@ -148,10 +177,9 @@ class HPGL2(Geometry):
def parse_lines(self, glines): def parse_lines(self, glines):
""" """
Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, Main HPGL2 parser.
``self.flashes``, ``self.regions`` and ``self.units``.
:param glines: Gerber code as list of strings, each element being :param glines: HPGL2 code as list of strings, each element being
one line of the source file. one line of the source file.
:type glines: list :type glines: list
:return: None :return: None
@ -159,33 +187,9 @@ class HPGL2(Geometry):
""" """
# Coordinates of the current path, each is [x, y] # Coordinates of the current path, each is [x, y]
path = [] path = list()
# this is for temporary storage of solid geometry until it is added to poly_buffer geo_buffer = []
geo_s = None
# this is for temporary storage of follow geometry until it is added to follow_buffer
geo_f = None
# Polygons are stored here until there is a change in polarity.
# Only then they are combined via cascaded_union and added or
# subtracted from solid_geometry. This is ~100 times faster than
# applying a union for every new polygon.
poly_buffer = []
# store here the follow geometry
follow_buffer = []
last_path_aperture = None
current_aperture = None
# 1,2 or 3 from "G01", "G02" or "G03"
current_interpolation_mode = None
# 1 or 2 from "D01" or "D02"
# Note this is to support deprecated Gerber not putting
# an operation code at the end of every coordinate line.
current_operation_code = None
# Current coordinates # Current coordinates
current_x = None current_x = None
@ -193,31 +197,17 @@ class HPGL2(Geometry):
previous_x = None previous_x = None
previous_y = None previous_y = None
current_d = None # store the pen (tool) status
pen_status = 'up'
# Absolute or Relative/Incremental coordinates # store the current tool here
# Not implemented current_tool = None
absolute = True
# How to interpret circular interpolation: SINGLE or MULTI
quadrant_mode = None
# Indicates we are parsing an aperture macro
current_macro = None
# Indicates the current polarity: D-Dark, C-Clear
current_polarity = 'D'
# If a region is being defined
making_region = False
# ### Parsing starts here ## ## # ### Parsing starts here ## ##
line_num = 0 line_num = 0
gline = "" gline = ""
s_tol = float(self.app.defaults["gerber_simp_tolerance"]) self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
try: try:
for gline in glines: for gline in glines:
if self.app.abort_flag: if self.app.abort_flag:
@ -235,466 +225,293 @@ class HPGL2(Geometry):
# Ignored lines ##### # Ignored lines #####
# Comments ##### # Comments #####
# ################### # ###################
match = self.comm_re.search(gline) match = self.comment_re.search(gline)
if match: if match:
log.debug(str(match.group(1)))
continue continue
# ## Mode (IN/MM) # #####################################################
# Example: %MOIN*% # Absolute/relative coordinates G90/1 OBSOLETE ########
match = self.mode_re.search(gline) # #####################################################
if match:
self.units = match.group(1)
log.debug("Gerber units found = %s" % self.units)
# Changed for issue #80
# self.convert_units(match.group(1))
self.conversion_done = True
continue
# ############################################################# ##
# Absolute/relative coordinates G90/1 OBSOLETE ######## ##
# ##################################################### ##
match = self.absrel_re.search(gline) match = self.absrel_re.search(gline)
if match: if match:
absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)] absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute) log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
continue continue
# ## G01 - Linear interpolation plus flashes # search for the initialization
# Operation code (D0x) missing is deprecated... oh well I will support it. match = self.initialize_re.search(gline)
# REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
match = self.lin_re.search(gline)
if match: if match:
# Parse coordinates self.in_header = False
if match.group(2) is not None:
linear_x = parse_number(match.group(2),
self.int_digits, self.frac_digits, self.gerber_zeros)
current_x = linear_x
else:
linear_x = current_x
if match.group(3) is not None:
linear_y = parse_number(match.group(3),
self.int_digits, self.frac_digits, self.gerber_zeros)
current_y = linear_y
else:
linear_y = current_y
# Parse operation code
if match.group(4) is not None:
current_operation_code = int(match.group(4))
# Pen down: add segment
if current_operation_code == 1:
# 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])
if making_region is False:
# if the aperture is rectangle then add a rectangular shape having as parameters the
# coordinates of the start and end point and also the width and height
# of the 'R' aperture
try:
if self.apertures[current_aperture]["type"] == 'R':
width = self.apertures[current_aperture]['width']
height = self.apertures[current_aperture]['height']
minx = min(path[0][0], path[1][0]) - width / 2
maxx = max(path[0][0], path[1][0]) + width / 2
miny = min(path[0][1], path[1][1]) - height / 2
maxy = max(path[0][1], path[1][1]) + height / 2
log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
geo_dict = dict()
geo_f = Point([current_x, current_y])
follow_buffer.append(geo_f)
geo_dict['follow'] = geo_f
geo_s = shply_box(minx, miny, maxx, maxy)
if self.app.defaults['gerber_simplification']:
poly_buffer.append(geo_s.simplify(s_tol))
else:
poly_buffer.append(geo_s)
if self.is_lpc is True:
geo_dict['clear'] = geo_s
else:
geo_dict['solid'] = geo_s
if current_aperture not in self.apertures:
self.apertures[current_aperture] = dict()
if 'geometry' not in self.apertures[current_aperture]:
self.apertures[current_aperture]['geometry'] = []
self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
except Exception as e:
pass
last_path_aperture = current_aperture
# we do this for the case that a region is done without having defined any aperture
if last_path_aperture is None:
if '0' not in self.apertures:
self.apertures['0'] = {}
self.apertures['0']['type'] = 'REG'
self.apertures['0']['size'] = 0.0
self.apertures['0']['geometry'] = []
last_path_aperture = '0'
else:
self.app.inform.emit('[WARNING] %s: %s' %
(_("Coordinates missing, line ignored"), str(gline)))
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("GERBER file might be CORRUPT. Check the file !!!"))
elif current_operation_code == 2:
if len(path) > 1:
geo_s = None
geo_dict = dict()
# --- BUFFERED ---
# this treats the case when we are storing geometry as paths only
if making_region:
# we do this for the case that a region is done without having defined any aperture
if last_path_aperture is None:
if '0' not in self.apertures:
self.apertures['0'] = {}
self.apertures['0']['type'] = 'REG'
self.apertures['0']['size'] = 0.0
self.apertures['0']['geometry'] = []
last_path_aperture = '0'
geo_f = Polygon()
else:
geo_f = LineString(path)
try:
if self.apertures[last_path_aperture]["type"] != 'R':
if not geo_f.is_empty:
follow_buffer.append(geo_f)
geo_dict['follow'] = geo_f
except Exception as e:
log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
if not geo_f.is_empty:
follow_buffer.append(geo_f)
geo_dict['follow'] = geo_f
# this treats the case when we are storing geometry as solids
if making_region:
# we do this for the case that a region is done without having defined any aperture
if last_path_aperture is None:
if '0' not in self.apertures:
self.apertures['0'] = {}
self.apertures['0']['type'] = 'REG'
self.apertures['0']['size'] = 0.0
self.apertures['0']['geometry'] = []
last_path_aperture = '0'
try:
geo_s = Polygon(path)
except ValueError:
log.warning("Problem %s %s" % (gline, line_num))
self.app.inform.emit('[ERROR] %s: %s' %
(_("Region does not have enough points. "
"File will be processed but there are parser errors. "
"Line number"), str(line_num)))
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
try:
if self.apertures[last_path_aperture]["type"] != 'R':
if not geo_s.is_empty:
if self.app.defaults['gerber_simplification']:
poly_buffer.append(geo_s.simplify(s_tol))
else:
poly_buffer.append(geo_s)
if self.is_lpc is True:
geo_dict['clear'] = geo_s
else:
geo_dict['solid'] = geo_s
except Exception as e:
log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
if self.app.defaults['gerber_simplification']:
poly_buffer.append(geo_s.simplify(s_tol))
else:
poly_buffer.append(geo_s)
if self.is_lpc is True:
geo_dict['clear'] = geo_s
else:
geo_dict['solid'] = geo_s
if last_path_aperture not in self.apertures:
self.apertures[last_path_aperture] = dict()
if 'geometry' not in self.apertures[last_path_aperture]:
self.apertures[last_path_aperture]['geometry'] = []
self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
# 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)))
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("GERBER file might be CORRUPT. Check the file !!!"))
# maybe those lines are not exactly needed but it is easier to read the program as those coordinates
# are used in case that circular interpolation is encountered within the Gerber file
current_x = linear_x
current_y = linear_y
# log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
continue continue
# ## G02/3 - Circular interpolation if self.in_header is False:
# 2-clockwise, 3-counterclockwise # tools detection
# Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point match = self.sp_re.search(gline)
match = self.circ_re.search(gline) if match:
if match: tool = match.group(1)
arcdir = [None, None, "cw", "ccw"] # self.tools[tool] = dict()
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()
}
})
mode, circular_x, circular_y, i, j, d = match.groups() if current_tool:
if path:
geo = LineString(path)
self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
path[:] = []
try: current_tool = tool
circular_x = parse_number(circular_x,
self.int_digits, self.frac_digits, self.gerber_zeros)
except Exception as e:
circular_x = current_x
try:
circular_y = parse_number(circular_y,
self.int_digits, self.frac_digits, self.gerber_zeros)
except Exception as e:
circular_y = current_y
# According to Gerber specification i and j are not modal, which means that when i or j are missing,
# they are to be interpreted as being zero
try:
i = parse_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
except Exception as e:
i = 0
try:
j = parse_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
except Exception as e:
j = 0
if quadrant_mode is None:
log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
log.error(gline)
continue continue
if mode is None and current_interpolation_mode not in [2, 3]: # pen status detection
log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) match = self.pen_re.search(gline)
log.error(gline) if match:
continue pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
elif mode is not None:
current_interpolation_mode = int(mode)
# Set operation code if provided
if d is not None:
current_operation_code = int(d)
# Nothing created! Pen Up.
if current_operation_code == 2:
log.warning("Arc with D2. (%d)" % line_num)
if len(path) > 1:
geo_dict = dict()
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
# --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"]
# this treats the case when we are storing geometry as paths
geo_f = LineString(path)
if not geo_f.is_empty:
follow_buffer.append(geo_f)
geo_dict['follow'] = geo_f
# this treats the case when we are storing geometry as solids
buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
if not buffered.is_empty:
if self.app.defaults['gerber_simplification']:
poly_buffer.append(buffered.simplify(s_tol))
else:
poly_buffer.append(buffered)
if self.is_lpc is True:
geo_dict['clear'] = buffered
else:
geo_dict['solid'] = buffered
if last_path_aperture not in self.apertures:
self.apertures[last_path_aperture] = dict()
if 'geometry' not in self.apertures[last_path_aperture]:
self.apertures[last_path_aperture]['geometry'] = []
self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
current_x = circular_x
current_y = circular_y
path = [[current_x, current_y]] # Start new path
continue continue
# Flash should not happen here # linear move
if current_operation_code == 3: match = self.abs_move_re.search(gline)
log.error("Trying to flash within arc. (%d)" % line_num) if match:
continue # Parse coordinates
if match.group(1) is not None:
if quadrant_mode == 'MULTI': linear_x = parse_number(match.group(1))
center = [i + current_x, j + current_y] current_x = linear_x
radius = np.sqrt(i ** 2 + j ** 2)
start = np.arctan2(-j, -i) # Start angle
# Numerical errors might prevent start == stop therefore
# we check ahead of time. This should result in a
# 360 degree arc.
if current_x == circular_x and current_y == circular_y:
stop = start
else: else:
stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle linear_x = current_x
this_arc = arc(center, radius, start, stop, if match.group(2) is not None:
arcdir[current_interpolation_mode], linear_y = parse_number(match.group(2))
self.steps_per_circle) current_y = linear_y
else:
linear_y = current_y
# The last point in the computed arc can have # Pen down: add segment
# numerical errors. The exact final point is the if pen_status == 'down':
# specified (x, y). Replace. # if linear_x or linear_y are None, ignore those
this_arc[-1] = (circular_x, circular_y) 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)))
# Last point in path is current point elif pen_status == 'up':
# current_x = this_arc[-1][0] if len(path) > 1:
# current_y = this_arc[-1][1] geo = LineString(path)
current_x, current_y = circular_x, circular_y self.tools[current_tool]['solid_geometry'].append(geo)
geo_buffer.append(geo)
# Append # if linear_x or linear_y are None, ignore those
path += this_arc if linear_x is not None and linear_y is not None:
last_path_aperture = current_aperture 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 continue
if quadrant_mode == 'SINGLE': # ## Circular interpolation
# -clockwise,
center_candidates = [ # -counterclockwise
[i + current_x, j + current_y], match = self.circ_re.search(gline)
[-i + current_x, j + current_y], # if match:
[i + current_x, -j + current_y], # arcdir = [None, None, "cw", "ccw"]
[-i + current_x, -j + current_y] #
] # mode, circular_x, circular_y, i, j, d = match.groups()
#
valid = False # try:
log.debug("I: %f J: %f" % (i, j)) # circular_x = parse_number(circular_x)
for center in center_candidates: # except Exception as e:
radius = np.sqrt(i ** 2 + j ** 2) # circular_x = current_x
#
# Make sure radius to start is the same as radius to end. # try:
radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2) # circular_y = parse_number(circular_y)
if radius2 < radius * 0.95 or radius2 > radius * 1.05: # except Exception as e:
continue # Not a valid center. # circular_y = current_y
#
# Correct i and j and continue as with multi-quadrant. # try:
i = center[0] - current_x # i = parse_number(i)
j = center[1] - current_y # except Exception as e:
# i = 0
start = np.arctan2(-j, -i) # Start angle #
stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle # try:
angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode])) # j = parse_number(j)
log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" % # except Exception as e:
(current_x, current_y, center[0], center[1], circular_x, circular_y)) # j = 0
log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" % #
(start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode], # if mode is None and current_interpolation_mode not in [2, 3]:
angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2)) # log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
# log.error(gline)
if angle <= (np.pi + 1e-6) / 2: # continue
log.debug("########## ACCEPTING ARC ############") # elif mode is not None:
this_arc = arc(center, radius, start, stop, # current_interpolation_mode = int(mode)
arcdir[current_interpolation_mode], #
self.steps_per_circle) # # Set operation code if provided
# if d is not None:
# Replace with exact values # current_operation_code = int(d)
this_arc[-1] = (circular_x, circular_y) #
# # Nothing created! Pen Up.
# current_x = this_arc[-1][0] # if current_operation_code == 2:
# current_y = this_arc[-1][1] # log.warning("Arc with D2. (%d)" % line_num)
current_x, current_y = circular_x, circular_y # if len(path) > 1:
# geo_dict = dict()
path += this_arc #
last_path_aperture = current_aperture # if last_path_aperture is None:
valid = True # log.warning("No aperture defined for curent path. (%d)" % line_num)
break #
# # --- BUFFERED ---
if valid: # width = self.apertures[last_path_aperture]["size"]
continue #
else: # # this treats the case when we are storing geometry as paths
log.warning("Invalid arc in line %d." % line_num) # geo_f = LineString(path)
# if not geo_f.is_empty:
# geo_dict['follow'] = geo_f
#
# # this treats the case when we are storing geometry as solids
# buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
#
# if last_path_aperture not in self.apertures:
# self.apertures[last_path_aperture] = dict()
# if 'geometry' not in self.apertures[last_path_aperture]:
# self.apertures[last_path_aperture]['geometry'] = []
# self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
#
# current_x = circular_x
# current_y = circular_y
# path = [[current_x, current_y]] # Start new path
# continue
#
# # Flash should not happen here
# if current_operation_code == 3:
# log.error("Trying to flash within arc. (%d)" % line_num)
# continue
#
# if quadrant_mode == 'MULTI':
# center = [i + current_x, j + current_y]
# radius = np.sqrt(i ** 2 + j ** 2)
# start = np.arctan2(-j, -i) # Start angle
# # Numerical errors might prevent start == stop therefore
# # we check ahead of time. This should result in a
# # 360 degree arc.
# if current_x == circular_x and current_y == circular_y:
# stop = start
# else:
# stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
#
# this_arc = arc(center, radius, start, stop,
# arcdir[current_interpolation_mode],
# self.steps_per_circle)
#
# # The last point in the computed arc can have
# # numerical errors. The exact final point is the
# # specified (x, y). Replace.
# this_arc[-1] = (circular_x, circular_y)
#
# # Last point in path is current point
# # current_x = this_arc[-1][0]
# # current_y = this_arc[-1][1]
# current_x, current_y = circular_x, circular_y
#
# # Append
# path += this_arc
# last_path_aperture = current_aperture
#
# continue
#
# if quadrant_mode == 'SINGLE':
#
# center_candidates = [
# [i + current_x, j + current_y],
# [-i + current_x, j + current_y],
# [i + current_x, -j + current_y],
# [-i + current_x, -j + current_y]
# ]
#
# valid = False
# log.debug("I: %f J: %f" % (i, j))
# for center in center_candidates:
# radius = np.sqrt(i ** 2 + j ** 2)
#
# # Make sure radius to start is the same as radius to end.
# radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
# if radius2 < radius * 0.95 or radius2 > radius * 1.05:
# continue # Not a valid center.
#
# # Correct i and j and continue as with multi-quadrant.
# i = center[0] - current_x
# j = center[1] - current_y
#
# start = np.arctan2(-j, -i) # Start angle
# stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
# angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
# log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
# (current_x, current_y, center[0], center[1], circular_x, circular_y))
# log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
# (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
# angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
#
# if angle <= (np.pi + 1e-6) / 2:
# log.debug("########## ACCEPTING ARC ############")
# this_arc = arc(center, radius, start, stop,
# arcdir[current_interpolation_mode],
# self.steps_per_circle)
#
# # Replace with exact values
# this_arc[-1] = (circular_x, circular_y)
#
# # current_x = this_arc[-1][0]
# # current_y = this_arc[-1][1]
# current_x, current_y = circular_x, circular_y
#
# path += this_arc
# last_path_aperture = current_aperture
# valid = True
# break
#
# if valid:
# continue
# else:
# log.warning("Invalid arc in line %d." % line_num)
# ## Line did not match any pattern. Warn user. # ## Line did not match any pattern. Warn user.
log.warning("Line ignored (%d): %s" % (line_num, gline)) log.warning("Line ignored (%d): %s" % (line_num, gline))
# --- Apply buffer --- if len(geo_buffer) == 0 and len(self.solid_geometry) == 0:
# this treats the case when we are storing geometry as paths log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
self.follow_geometry = follow_buffer
# this treats the case when we are storing geometry as solids
if len(poly_buffer) == 0 and len(self.solid_geometry) == 0:
log.error("Object is not Gerber file or empty. Aborting Object creation.")
return 'fail' return 'fail'
log.warning("Joining %d polygons." % len(poly_buffer)) log.warning("Joining %d polygons." % len(geo_buffer))
self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer))) self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
if self.use_buffer_for_union: new_poly = unary_union(geo_buffer)
log.debug("Union by buffer...") self.solid_geometry = new_poly
new_poly = MultiPolygon(poly_buffer)
if self.app.defaults["gerber_buffering"] == 'full':
new_poly = new_poly.buffer(0.00000001)
new_poly = new_poly.buffer(-0.00000001)
log.warning("Union(buffer) done.")
else:
log.debug("Union by union()...")
new_poly = cascaded_union(poly_buffer)
new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
log.warning("Union done.")
if current_polarity == 'D':
self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
if new_poly.is_valid:
self.solid_geometry = self.solid_geometry.union(new_poly)
else:
# I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
# loaded. Instead of applying a union I add to a list of polygons.
final_poly = []
try:
for poly in new_poly:
final_poly.append(poly)
except TypeError:
final_poly.append(new_poly)
try:
for poly in self.solid_geometry:
final_poly.append(poly)
except TypeError:
final_poly.append(self.solid_geometry)
self.solid_geometry = final_poly
else:
self.solid_geometry = self.solid_geometry.difference(new_poly)
# init this for the following operations
self.conversion_done = False
except Exception as err: except Exception as err:
ex_type, ex, tb = sys.exc_info() ex_type, ex, tb = sys.exc_info()
traceback.print_tb(tb) traceback.print_tb(tb)
# print traceback.format_exc() # print traceback.format_exc()
log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline)) log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err) loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
self.app.inform.emit('[ERROR] %s\n%s:' % self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
(_("Gerber Parser ERROR"), loc))
def create_geometry(self): def create_geometry(self):
""" """
@ -1245,5 +1062,5 @@ def parse_number(strnumber):
:rtype: float :rtype: float
""" """
return float(strnumber) * 40.0 # in milimeters return float(strnumber) / 40.0 # in milimeters