From 02b567971d7a92e6229f6379b6552c86206d2c1e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 12 Dec 2019 21:29:38 +0200 Subject: [PATCH] - finished a very rough and limited HPGL2 file import --- FlatCAMApp.py | 106 ++++- FlatCAMObj.py | 1 - README.md | 1 + flatcamGUI/FlatCAMGUI.py | 4 + flatcamParsers/ParseHPGL2.py | 809 ++++++++++++++--------------------- 5 files changed, 423 insertions(+), 498 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c706f35c..7caaf0da 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -63,6 +63,8 @@ from flatcamEditors.FlatCAMExcEditor import FlatCAMExcEditor from flatcamEditors.FlatCAMGrbEditor import FlatCAMGrbEditor from flatcamEditors.FlatCAMTextEditor import TextEditor +from flatcamParsers.ParseHPGL2 import HPGL2 + from FlatCAMProcess import * from FlatCAMWorkerStack import WorkerStack # 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_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.menufileexportpng.triggered.connect(self.on_file_exportpng) self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon) @@ -9295,6 +9297,44 @@ class App(QtCore.QObject): # thread safe. The new_project() 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): """ File menu callback for opening a config file. @@ -10931,6 +10971,70 @@ class App(QtCore.QObject): self.inform.emit('[success] %s: %s' % (_("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): """ Opens a Script file, parses it and creates a new object for diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 6cea67e8..5098fb1f 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -5744,7 +5744,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): def plot_element(self, element, color='#FF0000FF', visible=None): visible = visible if visible else self.options['plot'] - try: for sub_el in element: self.plot_element(sub_el) diff --git a/README.md b/README.md index f797e7b5..552a4259 100644 --- a/README.md +++ b/README.md @@ -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 - optimized the FlatCAMGerber.clear_plot_apertures() method - some changes in the ObjectUI and for the Geometry UI +- finished a very rough and limited HPGL2 file import 11.12.2019 diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index 5a951e87..596302dc 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -168,6 +168,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): _('&DXF as Gerber Object ...'), self) self.menufileimport.addAction(self.menufileimportdxf_as_gerber) 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 ... self.menufileexport = self.menufile.addMenu(QtGui.QIcon('share/export.png'), _('Export')) diff --git a/flatcamParsers/ParseHPGL2.py b/flatcamParsers/ParseHPGL2.py index fc6e315b..3798a8be 100644 --- a/flatcamParsers/ParseHPGL2.py +++ b/flatcamParsers/ParseHPGL2.py @@ -1,7 +1,7 @@ # ############################################################ # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # -# File Author: Marius Adrina Stanciu (c) # +# File Author: Marius Adrian Stanciu (c) # # Date: 12/11/2019 # # MIT Licence # # ############################################################ @@ -17,7 +17,7 @@ from copy import deepcopy import sys 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 from shapely.geometry import box as shply_box @@ -62,7 +62,54 @@ class HPGL2(Geometry): self.coord_mm_factor = 0.040 # 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 self.solid_geometry = None @@ -82,17 +129,17 @@ class HPGL2(Geometry): # comment self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$") # 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 self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$") # pen position self.pen_re = re.compile(r"^(P[U|D]);?$") # Initialize - self.mode_re = re.compile(r'^(IN);?$') + self.initialize_re = re.compile(r'^(IN);?$') + # select pen 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_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+))' + '?(?=.*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) self.absrel_re = re.compile(r'^G9([01])\*$') @@ -121,25 +162,13 @@ class HPGL2(Geometry): # in a Gerber file (normal or obsolete ones) 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:: - - G54D11*G36* - - First is ``G54D11*`` and seconds is ``G36*``. - - :param filename: Gerber file to parse. + :param filename: HPGL2 file to parse. :type filename: str - :param follow: If true, will not create polygons, just lines - following the gerber path. - :type follow: bool :return: None """ @@ -148,10 +177,9 @@ class HPGL2(Geometry): def parse_lines(self, glines): """ - Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, - ``self.flashes``, ``self.regions`` and ``self.units``. + Main HPGL2 parser. - :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. :type glines: list :return: None @@ -159,33 +187,9 @@ class HPGL2(Geometry): """ # 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_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 + geo_buffer = [] # Current coordinates current_x = None @@ -193,31 +197,17 @@ class HPGL2(Geometry): previous_x = None previous_y = None - current_d = None + # store the pen (tool) status + pen_status = 'up' - # Absolute or Relative/Incremental coordinates - # Not implemented - 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 + # store the current tool here + current_tool = None # ### Parsing starts here ## ## line_num = 0 gline = "" - s_tol = float(self.app.defaults["gerber_simp_tolerance"]) - - self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines"))) + self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines"))) try: for gline in glines: if self.app.abort_flag: @@ -235,466 +225,293 @@ class HPGL2(Geometry): # Ignored lines ##### # Comments ##### # ################### - match = self.comm_re.search(gline) + match = self.comment_re.search(gline) if match: + log.debug(str(match.group(1))) continue - # ## Mode (IN/MM) - # Example: %MOIN*% - 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 ######## ## - # ##################################################### ## + # ##################################################### + # Absolute/relative coordinates G90/1 OBSOLETE ######## + # ##################################################### match = self.absrel_re.search(gline) if match: absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)] log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute) continue - # ## G01 - Linear interpolation plus flashes - # Operation code (D0x) missing is deprecated... oh well I will support it. - # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$' - match = self.lin_re.search(gline) + # search for the initialization + match = self.initialize_re.search(gline) if match: - # Parse coordinates - 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)) + self.in_header = False continue - # ## G02/3 - Circular interpolation - # 2-clockwise, 3-counterclockwise - # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point - match = self.circ_re.search(gline) - if match: - arcdir = [None, None, "cw", "ccw"] + if self.in_header is False: + # tools detection + match = self.sp_re.search(gline) + if match: + tool = match.group(1) + # 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: - 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) + current_tool = tool continue - if mode is None and current_interpolation_mode not in [2, 3]: - log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) - log.error(gline) - continue - 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 + # pen status detection + match = self.pen_re.search(gline) + if match: + pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)] 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 + # linear move + 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: - stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle + linear_x = current_x - this_arc = arc(center, radius, start, stop, - arcdir[current_interpolation_mode], - self.steps_per_circle) + if match.group(2) is not None: + linear_y = parse_number(match.group(2)) + current_y = linear_y + else: + linear_y = current_y - # 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) + # 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))) - # 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 + elif pen_status == 'up': + if len(path) > 1: + geo = LineString(path) + self.tools[current_tool]['solid_geometry'].append(geo) + geo_buffer.append(geo) - # Append - path += this_arc - last_path_aperture = current_aperture + # 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 - 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) - + # ## Circular interpolation + # -clockwise, + # -counterclockwise + match = self.circ_re.search(gline) + # if match: + # arcdir = [None, None, "cw", "ccw"] + # + # mode, circular_x, circular_y, i, j, d = match.groups() + # + # try: + # circular_x = parse_number(circular_x) + # except Exception as e: + # circular_x = current_x + # + # try: + # circular_y = parse_number(circular_y) + # except Exception as e: + # circular_y = current_y + # + # try: + # i = parse_number(i) + # except Exception as e: + # i = 0 + # + # try: + # j = parse_number(j) + # except Exception as e: + # j = 0 + # + # if mode is None and current_interpolation_mode not in [2, 3]: + # log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) + # log.error(gline) + # continue + # 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: + # 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. log.warning("Line ignored (%d): %s" % (line_num, gline)) - # --- Apply buffer --- - # this treats the case when we are storing geometry as paths - 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.") + if len(geo_buffer) == 0 and len(self.solid_geometry) == 0: + log.error("Object is not HPGL2 file or empty. Aborting Object creation.") return 'fail' - log.warning("Joining %d polygons." % len(poly_buffer)) - self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer))) + log.warning("Joining %d polygons." % len(geo_buffer)) + self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer))) - if self.use_buffer_for_union: - log.debug("Union by buffer...") + new_poly = unary_union(geo_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: ex_type, ex, tb = sys.exc_info() traceback.print_tb(tb) # 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) - self.app.inform.emit('[ERROR] %s\n%s:' % - (_("Gerber Parser ERROR"), loc)) + 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 create_geometry(self): """ @@ -1245,5 +1062,5 @@ def parse_number(strnumber): :rtype: float """ - return float(strnumber) * 40.0 # in milimeters + return float(strnumber) / 40.0 # in milimeters