diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 5a56a3e7..364f2c48 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -16,6 +16,8 @@ from flatcamGUI.ObjectUI import * from FlatCAMCommon import LoudDict from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy from camlib import * +from flatcamParsers.ParseExcellon import Excellon +from flatcamParsers.ParseGerber import Gerber import itertools import tkinter as tk diff --git a/README.md b/README.md index 10be0f7c..9755d214 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ CAD program, and create G-Code for Isolation routing. - working in adding to the Optimal Tool the rest of the distances found in the Gerber and the locations associated; added GUI - added display of the results for the Rules Check Tool in a formatted way - made the Rules Check Tool document window Read Only +- made FlatCAMExcellon and FlatCAMGerber into their own files in the flatcamParser folder 5.10.2019 diff --git a/camlib.py b/camlib.py index 78902f29..16cffdc5 100644 --- a/camlib.py +++ b/camlib.py @@ -25,7 +25,6 @@ from rtree import index as rtindex from lxml import etree as ET # See: http://toblerity.org/shapely/manual.html - from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString from shapely.geometry import MultiPoint, MultiPolygon from shapely.geometry import box as shply_box @@ -54,15 +53,16 @@ import ezdxf from flatcamParsers.ParseSVG import * from flatcamParsers.ParseDXF import * +if platform.architecture()[0] == '64bit': + from ortools.constraint_solver import pywrapcp + from ortools.constraint_solver import routing_enums_pb2 + import logging import FlatCAMApp import gettext import FlatCAMTranslation as fcTranslate import builtins -if platform.architecture()[0] == '64bit': - from ortools.constraint_solver import pywrapcp - from ortools.constraint_solver import routing_enums_pb2 fcTranslate.apply_language('strings') @@ -2075,3406 +2075,6 @@ class ApertureMacro: return self.geometry -class Gerber (Geometry): - """ - Here it is done all the Gerber parsing. - - **ATTRIBUTES** - - * ``apertures`` (dict): The keys are names/identifiers of each aperture. - The values are dictionaries key/value pairs which describe the aperture. The - type key is always present and the rest depend on the key: - - +-----------+-----------------------------------+ - | Key | Value | - +===========+===================================+ - | type | (str) "C", "R", "O", "P", or "AP" | - +-----------+-----------------------------------+ - | others | Depend on ``type`` | - +-----------+-----------------------------------+ - | solid_geometry | (list) | - +-----------+-----------------------------------+ - * ``aperture_macros`` (dictionary): Are predefined geometrical structures - that can be instantiated with different parameters in an aperture - definition. See ``apertures`` above. The key is the name of the macro, - and the macro itself, the value, is a ``Aperture_Macro`` object. - - * ``flash_geometry`` (list): List of (Shapely) geometric object resulting - from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``. - - * ``buffered_paths`` (list): List of (Shapely) polygons resulting from - *buffering* (or thickening) the ``paths`` with the aperture. These are - generated from ``paths`` in ``buffer_paths()``. - - **USAGE**:: - - g = Gerber() - g.parse_file(filename) - g.create_geometry() - do_something(s.solid_geometry) - - """ - - # defaults = { - # "steps_per_circle": 128, - # "use_buffer_for_union": True - # } - - def __init__(self, steps_per_circle=None): - """ - The constructor takes no parameters. Use ``gerber.parse_files()`` - or ``gerber.parse_lines()`` to populate the object from Gerber source. - - :return: Gerber object - :rtype: Gerber - """ - - # How to approximate a circle with lines. - self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"]) - - # Initialize parent - Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) - - # Number format - self.int_digits = 3 - """Number of integer digits in Gerber numbers. Used during parsing.""" - - self.frac_digits = 4 - """Number of fraction digits in Gerber numbers. Used during parsing.""" - - self.gerber_zeros = self.app.defaults['gerber_def_zeros'] - """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing. - """ - - # ## Gerber elements # ## - ''' - apertures = { - 'id':{ - 'type':string, - 'size':float, - 'width':float, - 'height':float, - 'geometry': [], - } - } - apertures['geometry'] list elements are dicts - dict = { - 'solid': [], - 'follow': [], - 'clear': [] - } - ''' - - # store the file units here: - self.gerber_units = self.app.defaults['gerber_def_units'] - - # aperture storage - self.apertures = {} - - # Aperture Macros - self.aperture_macros = {} - - # will store the Gerber geometry's as solids - self.solid_geometry = Polygon() - - # will store the Gerber geometry's as paths - self.follow_geometry = [] - - # made True when the LPC command is encountered in Gerber parsing - # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict - self.is_lpc = False - - self.source_file = '' - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from Geometry. - self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', - 'aperture_macros', 'solid_geometry', 'source_file'] - - # ### Parser patterns ## ## - # FS - Format Specification - # The format of X and Y must be the same! - # L-omit leading zeros, T-omit trailing zeros, D-no zero supression - # A-absolute notation, I-incremental notation - self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\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\*%$') - - # Mode (IN/MM) - self.mode_re = re.compile(r'^%?MO(IN|MM)\*%?$') - - # Comment G04|G4 - self.comm_re = re.compile(r'^G0?4(.*)$') - - # AD - Aperture definition - # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+} - # NOTE: Adding "-" to support output from Upverter. - self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$') - - # AM - Aperture Macro - # Beginning of macro (Ends with *%): - # self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*') - - # Tool change - # May begin with G54 but that is deprecated - self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$') - - # G01... - Linear interpolation plus flashes with coordinates - # Operation code (D0x) missing is deprecated... oh well I will support it. - self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$') - - # Operation code alone, usually just D03 (Flash) - self.opcode_re = re.compile(r'^D0?([123])\*$') - - # G02/3... - Circular interpolation with coordinates - # 2-clockwise, 3-counterclockwise - # Operation code (D0x) missing is deprecated... oh well I will support it. - # Optional start with G02 or G03, optional end with D01 or D02 with - # optional coordinates but at least one in any order. - 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]).*\*$') - - # Region mode on - # In region mode, D01 starts a region - # and D02 ends it. A new region can be started again - # with D01. All contours must be closed before - # D02 or G37. - self.regionon_re = re.compile(r'^G36\*$') - - # Region mode off - # Will end a region and come off region mode. - # All contours must be closed before D02 or G37. - self.regionoff_re = re.compile(r'^G37\*$') - - # End of file - self.eof_re = re.compile(r'^M02\*') - - # IP - Image polarity - self.pol_re = re.compile(r'^%?IP(POS|NEG)\*%?$') - - # LP - Level polarity - self.lpol_re = re.compile(r'^%LP([DC])\*%$') - - # Units (OBSOLETE) - self.units_re = re.compile(r'^G7([01])\*$') - - # Absolute/Relative G90/1 (OBSOLETE) - self.absrel_re = re.compile(r'^G9([01])\*$') - - # Aperture macros - self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$') - self.am2_re = re.compile(r'(.*)%$') - - self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"] - - def aperture_parse(self, apertureId, apertureType, apParameters): - """ - Parse gerber aperture definition into dictionary of apertures. - The following kinds and their attributes are supported: - - * *Circular (C)*: size (float) - * *Rectangle (R)*: width (float), height (float) - * *Obround (O)*: width (float), height (float). - * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)] - * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list) - - :param apertureId: Id of the aperture being defined. - :param apertureType: Type of the aperture. - :param apParameters: Parameters of the aperture. - :type apertureId: str - :type apertureType: str - :type apParameters: str - :return: Identifier of the aperture. - :rtype: str - """ - if self.app.abort_flag: - # graceful abort requested by the user - raise FlatCAMApp.GracefulException - - # Found some Gerber with a leading zero in the aperture id and the - # referenced it without the zero, so this is a hack to handle that. - apid = str(int(apertureId)) - - try: # Could be empty for aperture macros - paramList = apParameters.split('X') - except: - paramList = None - - if apertureType == "C": # Circle, example: %ADD11C,0.1*% - self.apertures[apid] = {"type": "C", - "size": float(paramList[0])} - return apid - - if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*% - self.apertures[apid] = {"type": "R", - "width": float(paramList[0]), - "height": float(paramList[1]), - "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack - return apid - - if apertureType == "O": # Obround - self.apertures[apid] = {"type": "O", - "width": float(paramList[0]), - "height": float(paramList[1]), - "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack - return apid - - if apertureType == "P": # Polygon (regular) - self.apertures[apid] = {"type": "P", - "diam": float(paramList[0]), - "nVertices": int(paramList[1]), - "size": float(paramList[0])} # Hack - if len(paramList) >= 3: - self.apertures[apid]["rotation"] = float(paramList[2]) - return apid - - if apertureType in self.aperture_macros: - self.apertures[apid] = {"type": "AM", - "macro": self.aperture_macros[apertureType], - "modifiers": paramList} - return apid - - log.warning("Aperture not implemented: %s" % str(apertureType)) - return None - - def parse_file(self, filename, follow=False): - """ - 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. - :type filename: str - :param follow: If true, will not create polygons, just lines - following the gerber path. - :type follow: bool - :return: None - """ - - with open(filename, 'r') as gfile: - - def line_generator(): - for line in gfile: - line = line.strip(' \r\n') - while len(line) > 0: - - # If ends with '%' leave as is. - if line[-1] == '%': - yield line - break - - # Split after '*' if any. - starpos = line.find('*') - if starpos > -1: - cleanline = line[:starpos + 1] - yield cleanline - line = line[starpos + 1:] - - # Otherwise leave as is. - else: - # yield clean line - yield line - break - - processed_lines = list(line_generator()) - self.parse_lines(processed_lines) - - # @profile - def parse_lines(self, glines): - """ - Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, - ``self.flashes``, ``self.regions`` and ``self.units``. - - :param glines: Gerber 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 = [] - - # 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 - - # Current coordinates - current_x = None - current_y = None - previous_x = None - previous_y = None - - current_d = None - - # 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 - - # ### 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"))) - try: - for gline in glines: - if self.app.abort_flag: - # graceful abort requested by the user - raise FlatCAMApp.GracefulException - - 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.comm_re.search(gline) - if match: - continue - - # Polarity change ###### ## - # Example: %LPD*% or %LPC*% - # If polarity changes, creates geometry from current - # buffer, then adds or subtracts accordingly. - match = self.lpol_re.search(gline) - if match: - new_polarity = match.group(1) - # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer)) - self.is_lpc = True if new_polarity == 'C' else False - if len(path) > 1 and current_polarity != new_polarity: - - # finish the current path and add it to the storage - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - - geo_dict = dict() - geo_f = LineString(path) - if not geo_f.is_empty: - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - - geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - 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 - - 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)) - - path = [path[-1]] - - # --- Apply buffer --- - # If added for testing of bug #83 - # TODO: Remove when bug fixed - if len(poly_buffer) > 0: - if current_polarity == 'D': - # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer)) - self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) - - else: - # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer)) - self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) - - # follow_buffer = [] - poly_buffer = [] - - current_polarity = new_polarity - continue - - # ############################################################# ## - # Number format ############################################### ## - # Example: %FSLAX24Y24*% - # ############################################################# ## - # TODO: This is ignoring most of the format. Implement the rest. - match = self.fmt_re.search(gline) - if match: - absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)] - if match.group(1) is not None: - self.gerber_zeros = match.group(1) - self.int_digits = int(match.group(3)) - self.frac_digits = int(match.group(4)) - log.debug("Gerber format found. (%s) " % str(gline)) - - log.debug( - "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " - "D-no zero supression)" % self.gerber_zeros) - log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) - continue - - # ## Mode (IN/MM) - # Example: %MOIN*% - match = self.mode_re.search(gline) - if match: - self.gerber_units = match.group(1) - log.debug("Gerber units found = %s" % self.gerber_units) - # Changed for issue #80 - self.convert_units(match.group(1)) - continue - - # ############################################################# ## - # Combined Number format and Mode --- Allegro does this ####### ## - # ############################################################# ## - match = self.fmt_re_alt.search(gline) - if match: - absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)] - if match.group(1) is not None: - self.gerber_zeros = match.group(1) - self.int_digits = int(match.group(3)) - self.frac_digits = int(match.group(4)) - log.debug("Gerber format found. (%s) " % str(gline)) - log.debug( - "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " - "D-no zero suppression)" % self.gerber_zeros) - log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) - - self.gerber_units = match.group(5) - log.debug("Gerber units found = %s" % self.gerber_units) - # Changed for issue #80 - self.convert_units(match.group(5)) - continue - - # ############################################################# ## - # Search for OrCAD way for having Number format - # ############################################################# ## - match = self.fmt_re_orcad.search(gline) - if match: - if match.group(1) is not None: - if match.group(1) == 'G74': - quadrant_mode = 'SINGLE' - elif match.group(1) == 'G75': - quadrant_mode = 'MULTI' - absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)] - if match.group(2) is not None: - self.gerber_zeros = match.group(2) - - self.int_digits = int(match.group(4)) - self.frac_digits = int(match.group(5)) - log.debug("Gerber format found. (%s) " % str(gline)) - log.debug( - "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " - "D-no zerosuppressionn)" % self.gerber_zeros) - log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) - - self.gerber_units = match.group(1) - log.debug("Gerber units found = %s" % self.gerber_units) - # Changed for issue #80 - self.convert_units(match.group(5)) - continue - - # ############################################################# ## - # Units (G70/1) OBSOLETE - # ############################################################# ## - match = self.units_re.search(gline) - if match: - obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)] - log.warning("Gerber obsolete units found = %s" % obs_gerber_units) - # Changed for issue #80 - self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)]) - continue - - # ############################################################# ## - # 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 - - # ############################################################# ## - # Aperture Macros ##################################### ## - # Having this at the beginning will slow things down - # but macros can have complicated statements than could - # be caught by other patterns. - # ############################################################# ## - if current_macro is None: # No macro started yet - match = self.am1_re.search(gline) - # Start macro if match, else not an AM, carry on. - if match: - log.debug("Starting macro. Line %d: %s" % (line_num, gline)) - current_macro = match.group(1) - self.aperture_macros[current_macro] = ApertureMacro(name=current_macro) - if match.group(2): # Append - self.aperture_macros[current_macro].append(match.group(2)) - if match.group(3): # Finish macro - # self.aperture_macros[current_macro].parse_content() - current_macro = None - log.debug("Macro complete in 1 line.") - continue - else: # Continue macro - log.debug("Continuing macro. Line %d." % line_num) - match = self.am2_re.search(gline) - if match: # Finish macro - log.debug("End of macro. Line %d." % line_num) - self.aperture_macros[current_macro].append(match.group(1)) - # self.aperture_macros[current_macro].parse_content() - current_macro = None - else: # Append - self.aperture_macros[current_macro].append(gline) - continue - - # ## Aperture definitions %ADD... - match = self.ad_re.search(gline) - if match: - # log.info("Found aperture definition. Line %d: %s" % (line_num, gline)) - self.aperture_parse(match.group(1), match.group(2), match.group(3)) - continue - - # ############################################################# ## - # Operation code alone ###################### ## - # Operation code alone, usually just D03 (Flash) - # self.opcode_re = re.compile(r'^D0?([123])\*$') - # ############################################################# ## - match = self.opcode_re.search(gline) - if match: - current_operation_code = int(match.group(1)) - current_d = current_operation_code - - if current_operation_code == 3: - - # --- Buffered --- - try: - log.debug("Bare op-code %d." % current_operation_code) - geo_dict = dict() - flash = self.create_flash_geometry( - Point(current_x, current_y), self.apertures[current_aperture], - self.steps_per_circle) - - geo_dict['follow'] = Point([current_x, current_y]) - - if not flash.is_empty: - if self.app.defaults['gerber_simplification']: - poly_buffer.append(flash.simplify(s_tol)) - else: - poly_buffer.append(flash) - if self.is_lpc is True: - geo_dict['clear'] = flash - else: - geo_dict['solid'] = flash - - 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 IndexError: - log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline)) - - continue - - # ############################################################# ## - # Tool/aperture change - # Example: D12* - # ############################################################# ## - match = self.tool_re.search(gline) - if match: - current_aperture = match.group(1) - # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture)) - - # If the aperture value is zero then make it something quite small but with a non-zero value - # so it can be processed by FlatCAM. - # But first test to see if the aperture type is "aperture macro". In that case - # we should not test for "size" key as it does not exist in this case. - if self.apertures[current_aperture]["type"] is not "AM": - if self.apertures[current_aperture]["size"] == 0: - self.apertures[current_aperture]["size"] = 1e-12 - # log.debug(self.apertures[current_aperture]) - - # Take care of the current path with the previous tool - if len(path) > 1: - if self.apertures[last_path_aperture]["type"] == 'R': - # do nothing because 'R' type moving aperture is none at once - pass - else: - geo_dict = dict() - geo_f = LineString(path) - if not geo_f.is_empty: - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - 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 - - 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)) - - path = [path[-1]] - - continue - - # ############################################################# ## - # G36* - Begin region - # ############################################################# ## - if self.regionon_re.search(gline): - if len(path) > 1: - # Take care of what is left in the path - - geo_dict = dict() - geo_f = LineString(path) - if not geo_f.is_empty: - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - - # --- Buffered ---- - width = self.apertures[last_path_aperture]["size"] - geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - 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 - - 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)) - - path = [path[-1]] - - making_region = True - continue - - # ############################################################# ## - # G37* - End region - # ############################################################# ## - if self.regionoff_re.search(gline): - making_region = False - - if '0' not in self.apertures: - self.apertures['0'] = {} - self.apertures['0']['type'] = 'REG' - self.apertures['0']['size'] = 0.0 - self.apertures['0']['geometry'] = [] - - # if D02 happened before G37 we now have a path with 1 element only; we have to add the current - # geo to the poly_buffer otherwise we loose it - if current_operation_code == 2: - if len(path) == 1: - # this means that the geometry was prepared previously and we just need to add it - geo_dict = dict() - if geo_f: - if not geo_f.is_empty: - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - if geo_s: - 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 - - if geo_s or geo_f: - self.apertures['0']['geometry'].append(deepcopy(geo_dict)) - - path = [[current_x, current_y]] # Start new path - - # Only one path defines region? - # This can happen if D02 happened before G37 and - # is not and error. - if len(path) < 3: - # print "ERROR: Path contains less than 3 points:" - # path = [[current_x, current_y]] - continue - - # For regions we may ignore an aperture that is None - - # --- Buffered --- - geo_dict = dict() - region_f = Polygon(path).exterior - if not region_f.is_empty: - follow_buffer.append(region_f) - geo_dict['follow'] = region_f - - region_s = Polygon(path) - if not region_s.is_valid: - region_s = region_s.buffer(0, int(self.steps_per_circle / 4)) - - if not region_s.is_empty: - if self.app.defaults['gerber_simplification']: - poly_buffer.append(region_s.simplify(s_tol)) - else: - poly_buffer.append(region_s) - if self.is_lpc is True: - geo_dict['clear'] = region_s - else: - geo_dict['solid'] = region_s - - if not region_s.is_empty or not region_f.is_empty: - self.apertures['0']['geometry'].append(deepcopy(geo_dict)) - - path = [[current_x, current_y]] # Start new path - continue - - # ## G01/2/3* - Interpolation mode change - # Can occur along with coordinates and operation code but - # sometimes by itself (handled here). - # Example: G01* - match = self.interp_re.search(gline) - if match: - current_interpolation_mode = int(match.group(1)) - 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) - if match: - # Dxx alone? - # if match.group(1) is None and match.group(2) is None and match.group(3) is None: - # try: - # current_operation_code = int(match.group(4)) - # except: - # pass # A line with just * will match too. - # continue - # NOTE: Letting it continue allows it to react to the - # operation code. - - # Parse coordinates - if match.group(2) is not None: - linear_x = parse_gerber_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_gerber_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_f = 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 !!!")) - - # Flash - # Not allowed in region mode. - elif current_operation_code == 3: - - # Create path draw so far. - if len(path) > 1: - # --- Buffered ---- - geo_dict = dict() - - # this treats the case when we are storing geometry as paths - geo_f = LineString(path) - if not geo_f.is_empty: - try: - if self.apertures[last_path_aperture]["type"] != 'R': - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - except Exception as e: - log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e)) - follow_buffer.append(geo_f) - geo_dict['follow'] = geo_f - - # this treats the case when we are storing geometry as solids - width = self.apertures[last_path_aperture]["size"] - geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - if not geo_s.is_empty: - try: - if self.apertures[last_path_aperture]["type"] != 'R': - 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: - 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)) - - # Reset path starting point - path = [[linear_x, linear_y]] - - # --- BUFFERED --- - # Draw the flash - # this treats the case when we are storing geometry as paths - geo_dict = dict() - geo_flash = Point([linear_x, linear_y]) - follow_buffer.append(geo_flash) - geo_dict['follow'] = geo_flash - - # this treats the case when we are storing geometry as solids - flash = self.create_flash_geometry( - Point([linear_x, linear_y]), - self.apertures[current_aperture], - self.steps_per_circle - ) - if not flash.is_empty: - if self.app.defaults['gerber_simplification']: - poly_buffer.append(flash.simplify(s_tol)) - else: - poly_buffer.append(flash) - - if self.is_lpc is True: - geo_dict['clear'] = flash - else: - geo_dict['solid'] = flash - - 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)) - - # 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 - - # ## G74/75* - Single or multiple quadrant arcs - match = self.quad_re.search(gline) - if match: - if match.group(1) == '4': - quadrant_mode = 'SINGLE' - else: - quadrant_mode = 'MULTI' - 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"] - - mode, circular_x, circular_y, i, j, d = match.groups() - - try: - circular_x = parse_gerber_number(circular_x, - self.int_digits, self.frac_digits, self.gerber_zeros) - except: - circular_x = current_x - - try: - circular_y = parse_gerber_number(circular_y, - self.int_digits, self.frac_digits, self.gerber_zeros) - except: - 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_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros) - except: - i = 0 - - try: - j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros) - except: - 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 - - 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 - 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 = sqrt(i ** 2 + j ** 2) - start = 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 = 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 = sqrt(i ** 2 + j ** 2) - - # Make sure radius to start is the same as radius to end. - radius2 = 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 = arctan2(-j, -i) # Start angle - stop = 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 / pi, stop * 180 / pi, arcdir[current_interpolation_mode], - angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2)) - - if angle <= (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) - - # ## EOF - match = self.eof_re.search(gline) - if match: - continue - - # ## Line did not match any pattern. Warn user. - log.warning("Line ignored (%d): %s" % (line_num, gline)) - - if len(path) > 1: - # In case that G01 (moving) aperture is rectangular, there is no need to still create - # another geo since we already created a shapely box using the start and end coordinates found in - # path variable. We do it only for other apertures than 'R' type - if self.apertures[last_path_aperture]["type"] == 'R': - pass - else: - # EOF, create shapely LineString if something still in path - # ## --- Buffered --- - - geo_dict = dict() - # 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 - width = self.apertures[last_path_aperture]["size"] - geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - 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 - - 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)) - - # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way - # find out the conversion factor used to convert inside the self.apertures keys: size, width, height - file_units = self.gerber_units if self.gerber_units else 'IN' - app_units = self.app.defaults['units'] - - conversion_factor = 25.4 if file_units == 'IN' else (1/25.4) if file_units != app_units else 1 - - # --- 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: - log.error("Object is not Gerber 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))) - - if self.use_buffer_for_union: - log.debug("Union by buffer...") - - 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 - - # try: - # self.solid_geometry = self.solid_geometry.union(new_poly) - # except Exception as e: - # # in case in the new_poly are some self intersections try to avoid making union with them - # for poly in new_poly: - # try: - # self.solid_geometry = self.solid_geometry.union(poly) - # except: - # pass - else: - self.solid_geometry = self.solid_geometry.difference(new_poly) - 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)) - - 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)) - - @staticmethod - def create_flash_geometry(location, aperture, steps_per_circle=None): - - # log.debug('Flashing @%s, Aperture: %s' % (location, aperture)) - - if type(location) == list: - location = Point(location) - - if aperture['type'] == 'C': # Circles - return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4)) - - if aperture['type'] == 'R': # Rectangles - loc = location.coords[0] - width = aperture['width'] - height = aperture['height'] - minx = loc[0] - width / 2 - maxx = loc[0] + width / 2 - miny = loc[1] - height / 2 - maxy = loc[1] + height / 2 - return shply_box(minx, miny, maxx, maxy) - - if aperture['type'] == 'O': # Obround - loc = location.coords[0] - width = aperture['width'] - height = aperture['height'] - if width > height: - p1 = Point(loc[0] + 0.5 * (width - height), loc[1]) - p2 = Point(loc[0] - 0.5 * (width - height), loc[1]) - c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4)) - c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4)) - else: - p1 = Point(loc[0], loc[1] + 0.5 * (height - width)) - p2 = Point(loc[0], loc[1] - 0.5 * (height - width)) - c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4)) - c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4)) - return cascaded_union([c1, c2]).convex_hull - - if aperture['type'] == 'P': # Regular polygon - loc = location.coords[0] - diam = aperture['diam'] - n_vertices = aperture['nVertices'] - points = [] - for i in range(0, n_vertices): - x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices)) - y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices)) - points.append((x, y)) - ply = Polygon(points) - if 'rotation' in aperture: - ply = affinity.rotate(ply, aperture['rotation']) - return ply - - if aperture['type'] == 'AM': # Aperture Macro - loc = location.coords[0] - flash_geo = aperture['macro'].make_geometry(aperture['modifiers']) - if flash_geo.is_empty: - log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name)) - return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1]) - - log.warning("Unknown aperture type: %s" % aperture['type']) - return None - - def create_geometry(self): - """ - Geometry from a Gerber file is made up entirely of polygons. - Every stroke (linear or circular) has an aperture which gives - it thickness. Additionally, aperture strokes have non-zero area, - and regions naturally do as well. - - :rtype : None - :return: None - """ - pass - # self.buffer_paths() - # - # self.fix_regions() - # - # self.do_flashes() - # - # self.solid_geometry = cascaded_union(self.buffered_paths + - # [poly['polygon'] for poly in self.regions] + - # self.flash_geometry) - - def get_bounding_box(self, margin=0.0, rounded=False): - """ - Creates and returns a rectangular polygon bounding at a distance of - margin from the object's ``solid_geometry``. If margin > 0, the polygon - can optionally have rounded corners of radius equal to margin. - - :param margin: Distance to enlarge the rectangular bounding - box in both positive and negative, x and y axes. - :type margin: float - :param rounded: Wether or not to have rounded corners. - :type rounded: bool - :return: The bounding box. - :rtype: Shapely.Polygon - """ - - bbox = self.solid_geometry.envelope.buffer(margin) - if not rounded: - bbox = bbox.envelope - return bbox - - def bounds(self): - """ - Returns coordinates of rectangular bounds - of Gerber geometry: (xmin, ymin, xmax, ymax). - """ - # fixed issue of getting bounds only for one level lists of objects - # now it can get bounds for nested lists of objects - - log.debug("camlib.Gerber.bounds()") - - if self.solid_geometry is None: - log.debug("solid_geometry is None") - return 0, 0, 0, 0 - - def bounds_rec(obj): - if type(obj) is list and type(obj) is not MultiPolygon: - minx = Inf - miny = Inf - maxx = -Inf - maxy = -Inf - - for k in obj: - if type(k) is dict: - for key in k: - minx_, miny_, maxx_, maxy_ = bounds_rec(k[key]) - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - else: - if not k.is_empty: - try: - minx_, miny_, maxx_, maxy_ = bounds_rec(k) - except Exception as e: - log.debug("camlib.Gerber.bounds() --> %s" % str(e)) - return - - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - return minx, miny, maxx, maxy - else: - # it's a Shapely object, return it's bounds - return obj.bounds - - bounds_coords = bounds_rec(self.solid_geometry) - return bounds_coords - - def scale(self, xfactor, yfactor=None, point=None): - """ - Scales the objects' geometry on the XY plane by a given factor. - These are: - - * ``buffered_paths`` - * ``flash_geometry`` - * ``solid_geometry`` - * ``regions`` - - NOTE: - Does not modify the data used to create these elements. If these - are recreated, the scaling will be lost. This behavior was modified - because of the complexity reached in this class. - - :param xfactor: Number by which to scale on X axis. - :type xfactor: float - :param yfactor: Number by which to scale on Y axis. - :type yfactor: float - :rtype : None - """ - log.debug("camlib.Gerber.scale()") - - try: - xfactor = float(xfactor) - except: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Scale factor has to be a number: integer or float.")) - return - - if yfactor is None: - yfactor = xfactor - else: - try: - yfactor = float(yfactor) - except: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Scale factor has to be a number: integer or float.")) - return - - if point is None: - px = 0 - py = 0 - else: - px, py = point - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.solid_geometry: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - - self.old_disp_number = 0 - self.el_count = 0 - - def scale_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(scale_geom(g)) - return new_obj - else: - try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return affinity.scale(obj, xfactor, yfactor, origin=(px, py)) - except AttributeError: - return obj - - self.solid_geometry = scale_geom(self.solid_geometry) - self.follow_geometry = scale_geom(self.follow_geometry) - - # we need to scale the geometry stored in the Gerber apertures, too - try: - for apid in self.apertures: - if 'geometry' in self.apertures[apid]: - for geo_el in self.apertures[apid]['geometry']: - if 'solid' in geo_el: - geo_el['solid'] = scale_geom(geo_el['solid']) - if 'follow' in geo_el: - geo_el['follow'] = scale_geom(geo_el['follow']) - if 'clear' in geo_el: - geo_el['clear'] = scale_geom(geo_el['clear']) - - except Exception as e: - log.debug('camlib.Gerber.scale() Exception --> %s' % str(e)) - return 'fail' - - self.app.inform.emit('[success] %s' % - _("Gerber Scale done.")) - self.app.proc_container.new_text = '' - - # ## solid_geometry ??? - # It's a cascaded union of objects. - # self.solid_geometry = affinity.scale(self.solid_geometry, factor, - # factor, origin=(0, 0)) - - # # Now buffered_paths, flash_geometry and solid_geometry - # self.create_geometry() - - def offset(self, vect): - """ - Offsets the objects' geometry on the XY plane by a given vector. - These are: - - * ``buffered_paths`` - * ``flash_geometry`` - * ``solid_geometry`` - * ``regions`` - - NOTE: - Does not modify the data used to create these elements. If these - are recreated, the scaling will be lost. This behavior was modified - because of the complexity reached in this class. - - :param vect: (x, y) offset vector. - :type vect: tuple - :return: None - """ - log.debug("camlib.Gerber.offset()") - - try: - dx, dy = vect - except TypeError: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("An (x,y) pair of values are needed. " - "Probable you entered only one value in the Offset field.")) - return - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.solid_geometry: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - - self.old_disp_number = 0 - self.el_count = 0 - - def offset_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(offset_geom(g)) - return new_obj - else: - try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return affinity.translate(obj, xoff=dx, yoff=dy) - except AttributeError: - return obj - - # ## Solid geometry - self.solid_geometry = offset_geom(self.solid_geometry) - self.follow_geometry = offset_geom(self.follow_geometry) - - # we need to offset the geometry stored in the Gerber apertures, too - try: - for apid in self.apertures: - if 'geometry' in self.apertures[apid]: - for geo_el in self.apertures[apid]['geometry']: - if 'solid' in geo_el: - geo_el['solid'] = offset_geom(geo_el['solid']) - if 'follow' in geo_el: - geo_el['follow'] = offset_geom(geo_el['follow']) - if 'clear' in geo_el: - geo_el['clear'] = offset_geom(geo_el['clear']) - - except Exception as e: - log.debug('camlib.Gerber.offset() Exception --> %s' % str(e)) - return 'fail' - - self.app.inform.emit('[success] %s' % - _("Gerber Offset done.")) - self.app.proc_container.new_text = '' - - def mirror(self, axis, point): - """ - Mirrors the object around a specified axis passing through - the given point. What is affected: - - * ``buffered_paths`` - * ``flash_geometry`` - * ``solid_geometry`` - * ``regions`` - - NOTE: - Does not modify the data used to create these elements. If these - are recreated, the scaling will be lost. This behavior was modified - because of the complexity reached in this class. - - :param axis: "X" or "Y" indicates around which axis to mirror. - :type axis: str - :param point: [x, y] point belonging to the mirror axis. - :type point: list - :return: None - """ - log.debug("camlib.Gerber.mirror()") - - px, py = point - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.solid_geometry: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - - self.old_disp_number = 0 - self.el_count = 0 - - def mirror_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(mirror_geom(g)) - return new_obj - else: - try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return affinity.scale(obj, xscale, yscale, origin=(px, py)) - except AttributeError: - return obj - - self.solid_geometry = mirror_geom(self.solid_geometry) - self.follow_geometry = mirror_geom(self.follow_geometry) - - # we need to mirror the geometry stored in the Gerber apertures, too - try: - for apid in self.apertures: - if 'geometry' in self.apertures[apid]: - for geo_el in self.apertures[apid]['geometry']: - if 'solid' in geo_el: - geo_el['solid'] = mirror_geom(geo_el['solid']) - if 'follow' in geo_el: - geo_el['follow'] = mirror_geom(geo_el['follow']) - if 'clear' in geo_el: - geo_el['clear'] = mirror_geom(geo_el['clear']) - except Exception as e: - log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e)) - return 'fail' - - self.app.inform.emit('[success] %s' % - _("Gerber Mirror done.")) - self.app.proc_container.new_text = '' - - def skew(self, angle_x, angle_y, point): - """ - Shear/Skew the geometries of an object by angles along x and y dimensions. - - Parameters - ---------- - angle_x, angle_y : float, float - The shear angle(s) for the x and y axes respectively. These can be - specified in either degrees (default) or radians by setting - use_radians=True. - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - log.debug("camlib.Gerber.skew()") - - px, py = point - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.solid_geometry: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - - self.old_disp_number = 0 - self.el_count = 0 - - def skew_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(skew_geom(g)) - return new_obj - else: - try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return affinity.skew(obj, angle_x, angle_y, origin=(px, py)) - except AttributeError: - return obj - - self.solid_geometry = skew_geom(self.solid_geometry) - self.follow_geometry = skew_geom(self.follow_geometry) - - # we need to skew the geometry stored in the Gerber apertures, too - try: - for apid in self.apertures: - if 'geometry' in self.apertures[apid]: - for geo_el in self.apertures[apid]['geometry']: - if 'solid' in geo_el: - geo_el['solid'] = skew_geom(geo_el['solid']) - if 'follow' in geo_el: - geo_el['follow'] = skew_geom(geo_el['follow']) - if 'clear' in geo_el: - geo_el['clear'] = skew_geom(geo_el['clear']) - except Exception as e: - log.debug('camlib.Gerber.skew() Exception --> %s' % str(e)) - return 'fail' - - self.app.inform.emit('[success] %s' % - _("Gerber Skew done.")) - self.app.proc_container.new_text = '' - - def rotate(self, angle, point): - """ - Rotate an object by a given angle around given coords (point) - :param angle: - :param point: - :return: - """ - log.debug("camlib.Gerber.rotate()") - - px, py = point - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.solid_geometry: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - - self.old_disp_number = 0 - self.el_count = 0 - - def rotate_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(rotate_geom(g)) - return new_obj - else: - try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return affinity.rotate(obj, angle, origin=(px, py)) - except AttributeError: - return obj - - self.solid_geometry = rotate_geom(self.solid_geometry) - self.follow_geometry = rotate_geom(self.follow_geometry) - - # we need to rotate the geometry stored in the Gerber apertures, too - try: - for apid in self.apertures: - if 'geometry' in self.apertures[apid]: - for geo_el in self.apertures[apid]['geometry']: - if 'solid' in geo_el: - geo_el['solid'] = rotate_geom(geo_el['solid']) - if 'follow' in geo_el: - geo_el['follow'] = rotate_geom(geo_el['follow']) - if 'clear' in geo_el: - geo_el['clear'] = rotate_geom(geo_el['clear']) - except Exception as e: - log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e)) - return 'fail' - self.app.inform.emit('[success] %s' % - _("Gerber Rotate done.")) - self.app.proc_container.new_text = '' - - -class Excellon(Geometry): - """ - Here it is done all the Excellon parsing. - - *ATTRIBUTES* - - * ``tools`` (dict): The key is the tool name and the value is - a dictionary specifying the tool: - - ================ ==================================== - Key Value - ================ ==================================== - C Diameter of the tool - solid_geometry Geometry list for each tool - Others Not supported (Ignored). - ================ ==================================== - - * ``drills`` (list): Each is a dictionary: - - ================ ==================================== - Key Value - ================ ==================================== - point (Shapely.Point) Where to drill - tool (str) A key in ``tools`` - ================ ==================================== - - * ``slots`` (list): Each is a dictionary - - ================ ==================================== - Key Value - ================ ==================================== - start (Shapely.Point) Start point of the slot - stop (Shapely.Point) Stop point of the slot - tool (str) A key in ``tools`` - ================ ==================================== - """ - - defaults = { - "zeros": "L", - "excellon_format_upper_mm": '3', - "excellon_format_lower_mm": '3', - "excellon_format_upper_in": '2', - "excellon_format_lower_in": '4', - "excellon_units": 'INCH', - "geo_steps_per_circle": '64' - } - - def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None, - excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None, - geo_steps_per_circle=None): - """ - The constructor takes no parameters. - - :return: Excellon object. - :rtype: Excellon - """ - - if geo_steps_per_circle is None: - geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle']) - self.geo_steps_per_circle = int(geo_steps_per_circle) - - Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle)) - - # dictionary to store tools, see above for description - self.tools = {} - # list to store the drills, see above for description - self.drills = [] - - # self.slots (list) to store the slots; each is a dictionary - self.slots = [] - - self.source_file = '' - - # it serve to flag if a start routing or a stop routing was encountered - # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error - self.routing_flag = 1 - - self.match_routing_start = None - self.match_routing_stop = None - - self.num_tools = [] # List for keeping the tools sorted - self.index_per_tool = {} # Dictionary to store the indexed points for each tool - - # ## IN|MM -> Units are inherited from Geometry - # self.units = units - - # Trailing "T" or leading "L" (default) - # self.zeros = "T" - self.zeros = zeros or self.defaults["zeros"] - self.zeros_found = self.zeros - self.units_found = self.units - - # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be - # in another file like for PCB WIzard ECAD software - self.toolless_diam = 1.0 - # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter - self.diameterless = False - - # Excellon format - self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"] - self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"] - self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"] - self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"] - self.excellon_units = excellon_units or self.defaults["excellon_units"] - # detected Excellon format is stored here: - self.excellon_format = None - - # Attributes to be included in serialization - # Always append to it because it carries contents - # from Geometry. - self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm', - 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots', - 'source_file'] - - # ### Patterns #### - # Regex basics: - # ^ - beginning - # $ - end - # *: 0 or more, +: 1 or more, ?: 0 or 1 - - # M48 - Beginning of Part Program Header - self.hbegin_re = re.compile(r'^M48$') - - # ;HEADER - Beginning of Allegro Program Header - self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)') - - # M95 or % - End of Part Program Header - # NOTE: % has different meaning in the body - self.hend_re = re.compile(r'^(?:M95|%)$') - - # FMAT Excellon format - # Ignored in the parser - #self.fmat_re = re.compile(r'^FMAT,([12])$') - - # Uunits and possible Excellon zeros and possible Excellon format - # INCH uses 6 digits - # METRIC uses 5/6 - self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$') - - # Tool definition/parameters (?= is look-ahead - # NOTE: This might be an overkill! - # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' + - # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + - # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + - # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') - self.toolset_re = re.compile(r'^T(\d+)(?=.*C,?(\d*\.?\d*))?' + - r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + - r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + - r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') - - self.detect_gcode_re = re.compile(r'^G2([01])$') - - # Tool select - # Can have additional data after tool number but - # is ignored if present in the header. - # Warning: This will match toolset_re too. - # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))') - self.toolsel_re = re.compile(r'^T(\d+)') - - # Headerless toolset - # self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))') - self.toolset_hl_re = re.compile(r'^T(\d+)(?:.?C(\d+\.?\d*))?') - - # Comment - self.comm_re = re.compile(r'^;(.*)$') - - # Absolute/Incremental G90/G91 - self.absinc_re = re.compile(r'^G9([01])$') - - # Modes of operation - # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill - self.modes_re = re.compile(r'^G0([012345])') - - # Measuring mode - # 1-metric, 2-inch - self.meas_re = re.compile(r'^M7([12])$') - - # Coordinates - # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$') - # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$') - coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]' - self.coordsperiod_re = re.compile(coordsperiod_re_string) - - coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]' - self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string) - - # Slots parsing - slots_re_string = r'^([^G]+)G85(.*)$' - self.slots_re = re.compile(slots_re_string) - - # R - Repeat hole (# times, X offset, Y offset) - self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$') - - # Various stop/pause commands - self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))') - - # Allegro Excellon format support - self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))') - - # Altium Excellon format support - # it's a comment like this: ";FILE_FORMAT=2:5" - self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$') - - # Parse coordinates - self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)') - - # Repeating command - self.repeat_re = re.compile(r'R(\d+)') - - def parse_file(self, filename=None, file_obj=None): - """ - Reads the specified file as array of lines as - passes it to ``parse_lines()``. - - :param filename: The file to be read and parsed. - :type filename: str - :return: None - """ - if file_obj: - estr = file_obj - else: - if filename is None: - return "fail" - efile = open(filename, 'r') - estr = efile.readlines() - efile.close() - - try: - self.parse_lines(estr) - except: - return "fail" - - def parse_lines(self, elines): - """ - Main Excellon parser. - - :param elines: List of strings, each being a line of Excellon code. - :type elines: list - :return: None - """ - - # State variables - current_tool = "" - in_header = False - headerless = False - current_x = None - current_y = None - - slot_current_x = None - slot_current_y = None - - name_tool = 0 - allegro_warning = False - line_units_found = False - - repeating_x = 0 - repeating_y = 0 - repeat = 0 - - line_units = '' - - #### Parsing starts here ## ## - line_num = 0 # Line number - eline = "" - try: - for eline in elines: - if self.app.abort_flag: - # graceful abort requested by the user - raise FlatCAMApp.GracefulException - - line_num += 1 - # log.debug("%3d %s" % (line_num, str(eline))) - - self.source_file += eline - - # Cleanup lines - eline = eline.strip(' \r\n') - - # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe - # and we need to exit from here - if self.detect_gcode_re.search(eline): - log.warning("This is GCODE mark: %s" % eline) - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % - (_('This is GCODE mark'), eline)) - return - - # Header Begin (M48) # - if self.hbegin_re.search(eline): - in_header = True - headerless = False - log.warning("Found start of the header: %s" % eline) - continue - - # Allegro Header Begin (;HEADER) # - if self.allegro_hbegin_re.search(eline): - in_header = True - allegro_warning = True - log.warning("Found ALLEGRO start of the header: %s" % eline) - continue - - # Search for Header End # - # Since there might be comments in the header that include header end char (% or M95) - # we ignore the lines starting with ';' that contains such header end chars because it is not a - # real header end. - if self.comm_re.search(eline): - match = self.tool_units_re.search(eline) - if match: - if line_units_found is False: - line_units_found = True - line_units = match.group(3) - self.convert_units({"MILS": "IN", "MM": "MM"}[line_units]) - log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units) - - if match.group(2): - name_tool += 1 - if line_units == 'MILS': - spec = {"C": (float(match.group(2)) / 1000)} - self.tools[str(name_tool)] = spec - log.debug(" Tool definition: %s %s" % (name_tool, spec)) - else: - spec = {"C": float(match.group(2))} - self.tools[str(name_tool)] = spec - log.debug(" Tool definition: %s %s" % (name_tool, spec)) - spec['solid_geometry'] = [] - continue - # search for Altium Excellon Format / Sprint Layout who is included as a comment - match = self.altium_format.search(eline) - if match: - self.excellon_format_upper_mm = match.group(1) - self.excellon_format_lower_mm = match.group(2) - - self.excellon_format_upper_in = match.group(1) - self.excellon_format_lower_in = match.group(2) - log.warning("Altium Excellon format preset found in comments: %s:%s" % - (match.group(1), match.group(2))) - continue - else: - log.warning("Line ignored, it's a comment: %s" % eline) - else: - if self.hend_re.search(eline): - if in_header is False or bool(self.tools) is False: - log.warning("Found end of the header but there is no header: %s" % eline) - log.warning("The only useful data in header are tools, units and format.") - log.warning("Therefore we will create units and format based on defaults.") - headerless = True - try: - self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units]) - except Exception as e: - log.warning("Units could not be converted: %s" % str(e)) - - in_header = False - # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange - if allegro_warning is True: - name_tool = 0 - log.warning("Found end of the header: %s" % eline) - continue - - # ## Alternative units format M71/M72 - # Supposed to be just in the body (yes, the body) - # but some put it in the header (PADS for example). - # Will detect anywhere. Occurrence will change the - # object's units. - match = self.meas_re.match(eline) - if match: - # self.units = {"1": "MM", "2": "IN"}[match.group(1)] - - # Modified for issue #80 - self.convert_units({"1": "MM", "2": "IN"}[match.group(1)]) - log.debug(" Units: %s" % self.units) - if self.units == 'MM': - log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \ - ':' + str(self.excellon_format_lower_mm)) - else: - log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \ - ':' + str(self.excellon_format_lower_in)) - continue - - # ### Body #### - if not in_header: - - # ## Tool change ### - match = self.toolsel_re.search(eline) - if match: - current_tool = str(int(match.group(1))) - log.debug("Tool change: %s" % current_tool) - if bool(headerless): - match = self.toolset_hl_re.search(eline) - if match: - name = str(int(match.group(1))) - try: - diam = float(match.group(2)) - except: - # it's possible that tool definition has only tool number and no diameter info - # (those could be in another file like PCB Wizard do) - # then match.group(2) = None and float(None) will create the exception - # the bellow construction is so each tool will have a slightly different diameter - # starting with a default value, to allow Excellon editing after that - self.diameterless = True - self.app.inform.emit('[WARNING] %s%s %s' % - (_("No tool diameter info's. See shell.\n" - "A tool change event: T"), - str(current_tool), - _("was found but the Excellon file " - "have no informations regarding the tool " - "diameters therefore the application will try to load it " - "by using some 'fake' diameters.\n" - "The user needs to edit the resulting Excellon object and " - "change the diameters to reflect the real diameters.") - ) - ) - - if self.excellon_units == 'MM': - diam = self.toolless_diam + (int(current_tool) - 1) / 100 - else: - diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4 - - spec = {"C": diam, 'solid_geometry': []} - self.tools[name] = spec - log.debug("Tool definition out of header: %s %s" % (name, spec)) - - continue - - # ## Allegro Type Tool change ### - if allegro_warning is True: - match = self.absinc_re.search(eline) - match1 = self.stop_re.search(eline) - if match or match1: - name_tool += 1 - current_tool = str(name_tool) - log.debug("Tool change for Allegro type of Excellon: %s" % current_tool) - continue - - # ## Slots parsing for drilled slots (contain G85) - # a Excellon drilled slot line may look like this: - # X01125Y0022244G85Y0027756 - match = self.slots_re.search(eline) - if match: - # signal that there are milling slots operations - self.defaults['excellon_drills'] = False - - # the slot start coordinates group is to the left of G85 command (group(1) ) - # the slot stop coordinates group is to the right of G85 command (group(2) ) - start_coords_match = match.group(1) - stop_coords_match = match.group(2) - - # Slot coordinates without period # ## - # get the coordinates for slot start and for slot stop into variables - start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match) - stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match) - if start_coords_noperiod: - try: - slot_start_x = self.parse_number(start_coords_noperiod.group(1)) - slot_current_x = slot_start_x - except TypeError: - slot_start_x = slot_current_x - except: - return - - try: - slot_start_y = self.parse_number(start_coords_noperiod.group(2)) - slot_current_y = slot_start_y - except TypeError: - slot_start_y = slot_current_y - except: - return - - try: - slot_stop_x = self.parse_number(stop_coords_noperiod.group(1)) - slot_current_x = slot_stop_x - except TypeError: - slot_stop_x = slot_current_x - except: - return - - try: - slot_stop_y = self.parse_number(stop_coords_noperiod.group(2)) - slot_current_y = slot_stop_y - except TypeError: - slot_stop_y = slot_current_y - except: - return - - if (slot_start_x is None or slot_start_y is None or - slot_stop_x is None or slot_stop_y is None): - log.error("Slots are missing some or all coordinates.") - continue - - # we have a slot - log.debug('Parsed a slot with coordinates: ' + str([slot_start_x, - slot_start_y, slot_stop_x, - slot_stop_y])) - - # store current tool diameter as slot diameter - slot_dia = 0.05 - try: - slot_dia = float(self.tools[current_tool]['C']) - except Exception as e: - pass - log.debug( - 'Milling/Drilling slot with tool %s, diam=%f' % ( - current_tool, - slot_dia - ) - ) - - self.slots.append( - { - 'start': Point(slot_start_x, slot_start_y), - 'stop': Point(slot_stop_x, slot_stop_y), - 'tool': current_tool - } - ) - continue - - # Slot coordinates with period: Use literally. ### - # get the coordinates for slot start and for slot stop into variables - start_coords_period = self.coordsperiod_re.search(start_coords_match) - stop_coords_period = self.coordsperiod_re.search(stop_coords_match) - if start_coords_period: - - try: - slot_start_x = float(start_coords_period.group(1)) - slot_current_x = slot_start_x - except TypeError: - slot_start_x = slot_current_x - except: - return - - try: - slot_start_y = float(start_coords_period.group(2)) - slot_current_y = slot_start_y - except TypeError: - slot_start_y = slot_current_y - except: - return - - try: - slot_stop_x = float(stop_coords_period.group(1)) - slot_current_x = slot_stop_x - except TypeError: - slot_stop_x = slot_current_x - except: - return - - try: - slot_stop_y = float(stop_coords_period.group(2)) - slot_current_y = slot_stop_y - except TypeError: - slot_stop_y = slot_current_y - except: - return - - if (slot_start_x is None or slot_start_y is None or - slot_stop_x is None or slot_stop_y is None): - log.error("Slots are missing some or all coordinates.") - continue - - # we have a slot - log.debug('Parsed a slot with coordinates: ' + str([slot_start_x, - slot_start_y, slot_stop_x, slot_stop_y])) - - # store current tool diameter as slot diameter - slot_dia = 0.05 - try: - slot_dia = float(self.tools[current_tool]['C']) - except Exception as e: - pass - log.debug( - 'Milling/Drilling slot with tool %s, diam=%f' % ( - current_tool, - slot_dia - ) - ) - - self.slots.append( - { - 'start': Point(slot_start_x, slot_start_y), - 'stop': Point(slot_stop_x, slot_stop_y), - 'tool': current_tool - } - ) - continue - - # ## Coordinates without period # ## - match = self.coordsnoperiod_re.search(eline) - if match: - matchr = self.repeat_re.search(eline) - if matchr: - repeat = int(matchr.group(1)) - - try: - x = self.parse_number(match.group(1)) - repeating_x = current_x - current_x = x - except TypeError: - x = current_x - repeating_x = 0 - except: - return - - try: - y = self.parse_number(match.group(2)) - repeating_y = current_y - current_y = y - except TypeError: - y = current_y - repeating_y = 0 - except: - return - - if x is None or y is None: - log.error("Missing coordinates") - continue - - # ## Excellon Routing parse - if len(re.findall("G00", eline)) > 0: - self.match_routing_start = 'G00' - - # signal that there are milling slots operations - self.defaults['excellon_drills'] = False - - self.routing_flag = 0 - slot_start_x = x - slot_start_y = y - continue - - if self.routing_flag == 0: - if len(re.findall("G01", eline)) > 0: - self.match_routing_stop = 'G01' - - # signal that there are milling slots operations - self.defaults['excellon_drills'] = False - - self.routing_flag = 1 - slot_stop_x = x - slot_stop_y = y - self.slots.append( - { - 'start': Point(slot_start_x, slot_start_y), - 'stop': Point(slot_stop_x, slot_stop_y), - 'tool': current_tool - } - ) - continue - - if self.match_routing_start is None and self.match_routing_stop is None: - if repeat == 0: - # signal that there are drill operations - self.defaults['excellon_drills'] = True - self.drills.append({'point': Point((x, y)), 'tool': current_tool}) - else: - coordx = x - coordy = y - while repeat > 0: - if repeating_x: - coordx = (repeat * x) + repeating_x - if repeating_y: - coordy = (repeat * y) + repeating_y - self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) - repeat -= 1 - repeating_x = repeating_y = 0 - # log.debug("{:15} {:8} {:8}".format(eline, x, y)) - continue - - # ## Coordinates with period: Use literally. # ## - match = self.coordsperiod_re.search(eline) - if match: - matchr = self.repeat_re.search(eline) - if matchr: - repeat = int(matchr.group(1)) - - if match: - # signal that there are drill operations - self.defaults['excellon_drills'] = True - try: - x = float(match.group(1)) - repeating_x = current_x - current_x = x - except TypeError: - x = current_x - repeating_x = 0 - - try: - y = float(match.group(2)) - repeating_y = current_y - current_y = y - except TypeError: - y = current_y - repeating_y = 0 - - if x is None or y is None: - log.error("Missing coordinates") - continue - - # ## Excellon Routing parse - if len(re.findall("G00", eline)) > 0: - self.match_routing_start = 'G00' - - # signal that there are milling slots operations - self.defaults['excellon_drills'] = False - - self.routing_flag = 0 - slot_start_x = x - slot_start_y = y - continue - - if self.routing_flag == 0: - if len(re.findall("G01", eline)) > 0: - self.match_routing_stop = 'G01' - - # signal that there are milling slots operations - self.defaults['excellon_drills'] = False - - self.routing_flag = 1 - slot_stop_x = x - slot_stop_y = y - self.slots.append( - { - 'start': Point(slot_start_x, slot_start_y), - 'stop': Point(slot_stop_x, slot_stop_y), - 'tool': current_tool - } - ) - continue - - if self.match_routing_start is None and self.match_routing_stop is None: - # signal that there are drill operations - if repeat == 0: - # signal that there are drill operations - self.defaults['excellon_drills'] = True - self.drills.append({'point': Point((x, y)), 'tool': current_tool}) - else: - coordx = x - coordy = y - while repeat > 0: - if repeating_x: - coordx = (repeat * x) + repeating_x - if repeating_y: - coordy = (repeat * y) + repeating_y - self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) - repeat -= 1 - repeating_x = repeating_y = 0 - # log.debug("{:15} {:8} {:8}".format(eline, x, y)) - continue - - # ### Header #### - if in_header: - - # ## Tool definitions # ## - match = self.toolset_re.search(eline) - if match: - - name = str(int(match.group(1))) - spec = {"C": float(match.group(2)), 'solid_geometry': []} - self.tools[name] = spec - log.debug(" Tool definition: %s %s" % (name, spec)) - continue - - # ## Units and number format # ## - match = self.units_re.match(eline) - if match: - self.units_found = match.group(1) - self.zeros = match.group(2) # "T" or "L". Might be empty - self.excellon_format = match.group(3) - if self.excellon_format: - upper = len(self.excellon_format.partition('.')[0]) - lower = len(self.excellon_format.partition('.')[2]) - if self.units == 'MM': - self.excellon_format_upper_mm = upper - self.excellon_format_lower_mm = lower - else: - self.excellon_format_upper_in = upper - self.excellon_format_lower_in = lower - - # Modified for issue #80 - self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found]) - # log.warning(" Units/Format: %s %s" % (self.units, self.zeros)) - log.warning("Units: %s" % self.units) - if self.units == 'MM': - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + - ':' + str(self.excellon_format_lower_mm)) - else: - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + - ':' + str(self.excellon_format_lower_in)) - log.warning("Type of zeros found inline: %s" % self.zeros) - continue - - # Search for units type again it might be alone on the line - if "INCH" in eline: - line_units = "INCH" - # Modified for issue #80 - self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units]) - log.warning("Type of UNITS found inline: %s" % line_units) - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + - ':' + str(self.excellon_format_lower_in)) - # TODO: not working - #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline)) - continue - elif "METRIC" in eline: - line_units = "METRIC" - # Modified for issue #80 - self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units]) - log.warning("Type of UNITS found inline: %s" % line_units) - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + - ':' + str(self.excellon_format_lower_mm)) - # TODO: not working - #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline)) - continue - - # Search for zeros type again because it might be alone on the line - match = re.search(r'[LT]Z',eline) - if match: - self.zeros = match.group() - log.warning("Type of zeros found: %s" % self.zeros) - continue - - # ## Units and number format outside header# ## - match = self.units_re.match(eline) - if match: - self.units_found = match.group(1) - self.zeros = match.group(2) # "T" or "L". Might be empty - self.excellon_format = match.group(3) - if self.excellon_format: - upper = len(self.excellon_format.partition('.')[0]) - lower = len(self.excellon_format.partition('.')[2]) - if self.units == 'MM': - self.excellon_format_upper_mm = upper - self.excellon_format_lower_mm = lower - else: - self.excellon_format_upper_in = upper - self.excellon_format_lower_in = lower - - # Modified for issue #80 - self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found]) - # log.warning(" Units/Format: %s %s" % (self.units, self.zeros)) - log.warning("Units: %s" % self.units) - if self.units == 'MM': - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + - ':' + str(self.excellon_format_lower_mm)) - else: - log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + - ':' + str(self.excellon_format_lower_in)) - log.warning("Type of zeros found outside header, inline: %s" % self.zeros) - - log.warning("UNITS found outside header") - continue - - log.warning("Line ignored: %s" % eline) - - # make sure that since we are in headerless mode, we convert the tools only after the file parsing - # is finished since the tools definitions are spread in the Excellon body. We use as units the value - # from self.defaults['excellon_units'] - log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) - except Exception as e: - log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline)) - msg = '[ERROR_NOTCL] %s' % \ - _("An internal error has ocurred. See shell.\n") - msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format( - e_code='[ERROR]', - l_nr=line_num, - line=eline) - msg += traceback.format_exc() - self.app.inform.emit(msg) - - return "fail" - - def parse_number(self, number_str): - """ - Parses coordinate numbers without period. - - :param number_str: String representing the numerical value. - :type number_str: str - :return: Floating point representation of the number - :rtype: float - """ - - match = self.leadingzeros_re.search(number_str) - nr_length = len(match.group(1)) + len(match.group(2)) - try: - if self.zeros == "L" or self.zeros == "LZ": # Leading - # With leading zeros, when you type in a coordinate, - # the leading zeros must always be included. Trailing zeros - # are unneeded and may be left off. The CNC-7 will automatically add them. - # r'^[-\+]?(0*)(\d*)' - # 6 digits are divided by 10^4 - # If less than size digits, they are automatically added, - # 5 digits then are divided by 10^3 and so on. - - if self.units.lower() == "in": - result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in))) - else: - result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm))) - return result - else: # Trailing - # You must show all zeros to the right of the number and can omit - # all zeros to the left of the number. The CNC-7 will count the number - # of digits you typed and automatically fill in the missing zeros. - # ## flatCAM expects 6digits - # flatCAM expects the number of digits entered into the defaults - - if self.units.lower() == "in": # Inches is 00.0000 - result = float(number_str) / (10 ** (float(self.excellon_format_lower_in))) - else: # Metric is 000.000 - result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm))) - return result - except Exception as e: - log.error("Aborted. Operation could not be completed due of %s" % str(e)) - return - - def create_geometry(self): - """ - Creates circles of the tool diameter at every point - specified in ``self.drills``. Also creates geometries (polygons) - for the slots as specified in ``self.slots`` - All the resulting geometry is stored into self.solid_geometry list. - The list self.solid_geometry has 2 elements: first is a dict with the drills geometry, - and second element is another similar dict that contain the slots geometry. - - Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries - ================ ==================================== - Key Value - ================ ==================================== - tool_diameter list of (Shapely.Point) Where to drill - ================ ==================================== - - :return: None - """ - self.solid_geometry = [] - try: - # clear the solid_geometry in self.tools - for tool in self.tools: - try: - self.tools[tool]['solid_geometry'][:] = [] - except KeyError: - self.tools[tool]['solid_geometry'] = [] - - for drill in self.drills: - # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0) - if drill['tool'] is '': - self.app.inform.emit('[WARNING] %s' % - _("Excellon.create_geometry() -> a drill location was skipped " - "due of not having a tool associated.\n" - "Check the resulting GCode.")) - log.debug("Excellon.create_geometry() -> a drill location was skipped " - "due of not having a tool associated") - continue - tooldia = self.tools[drill['tool']]['C'] - poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4)) - self.solid_geometry.append(poly) - self.tools[drill['tool']]['solid_geometry'].append(poly) - - for slot in self.slots: - slot_tooldia = self.tools[slot['tool']]['C'] - start = slot['start'] - stop = slot['stop'] - - lines_string = LineString([start, stop]) - poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4)) - self.solid_geometry.append(poly) - self.tools[slot['tool']]['solid_geometry'].append(poly) - - except Exception as e: - log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e)) - return "fail" - - # drill_geometry = {} - # slot_geometry = {} - # - # def insertIntoDataStruct(dia, drill_geo, aDict): - # if not dia in aDict: - # aDict[dia] = [drill_geo] - # else: - # aDict[dia].append(drill_geo) - # - # for tool in self.tools: - # tooldia = self.tools[tool]['C'] - # for drill in self.drills: - # if drill['tool'] == tool: - # poly = drill['point'].buffer(tooldia / 2.0) - # insertIntoDataStruct(tooldia, poly, drill_geometry) - # - # for tool in self.tools: - # slot_tooldia = self.tools[tool]['C'] - # for slot in self.slots: - # if slot['tool'] == tool: - # start = slot['start'] - # stop = slot['stop'] - # lines_string = LineString([start, stop]) - # poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle) - # insertIntoDataStruct(slot_tooldia, poly, drill_geometry) - # - # self.solid_geometry = [drill_geometry, slot_geometry] - - def bounds(self): - """ - Returns coordinates of rectangular bounds - of Excellon geometry: (xmin, ymin, xmax, ymax). - """ - # fixed issue of getting bounds only for one level lists of objects - # now it can get bounds for nested lists of objects - - log.debug("camlib.Excellon.bounds()") - if self.solid_geometry is None: - log.debug("solid_geometry is None") - return 0, 0, 0, 0 - - def bounds_rec(obj): - if type(obj) is list: - minx = Inf - miny = Inf - maxx = -Inf - maxy = -Inf - - for k in obj: - if type(k) is dict: - for key in k: - minx_, miny_, maxx_, maxy_ = bounds_rec(k[key]) - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - else: - minx_, miny_, maxx_, maxy_ = bounds_rec(k) - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - return minx, miny, maxx, maxy - else: - # it's a Shapely object, return it's bounds - return obj.bounds - - minx_list = [] - miny_list = [] - maxx_list = [] - maxy_list = [] - - for tool in self.tools: - minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry']) - minx_list.append(minx) - miny_list.append(miny) - maxx_list.append(maxx) - maxy_list.append(maxy) - - return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)) - - def convert_units(self, units): - """ - This function first convert to the the units found in the Excellon file but it converts tools that - are not there yet so it has no effect other than it signal that the units are the ones in the file. - - On object creation, in new_object(), true conversion is done because this is done at the end of the - Excellon file parsing, the tools are inside and self.tools is really converted from the units found - inside the file to the FlatCAM units. - - Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file - will have detected the units before the tools are parsed and stored in self.tools - :param units: - :type str: IN or MM - :return: - """ - log.debug("camlib.Excellon.convert_units()") - - factor = Geometry.convert_units(self, units) - - # Tools - for tname in self.tools: - self.tools[tname]["C"] *= factor - - self.create_geometry() - - return factor - - def scale(self, xfactor, yfactor=None, point=None): - """ - Scales geometry on the XY plane in the object by a given factor. - Tool sizes, feedrates an Z-plane dimensions are untouched. - - :param factor: Number by which to scale the object. - :type factor: float - :return: None - :rtype: NOne - """ - log.debug("camlib.Excellon.scale()") - - if yfactor is None: - yfactor = xfactor - - if point is None: - px = 0 - py = 0 - else: - px, py = point - - def scale_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(scale_geom(g)) - return new_obj - else: - try: - return affinity.scale(obj, xfactor, yfactor, origin=(px, py)) - except AttributeError: - return obj - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.drills: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - - # Drills - for drill in self.drills: - drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py)) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # scale solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py)) - slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py)) - - self.create_geometry() - self.app.proc_container.new_text = '' - - def offset(self, vect): - """ - Offsets geometry on the XY plane in the object by a given vector. - - :param vect: (x, y) offset vector. - :type vect: tuple - :return: None - """ - log.debug("camlib.Excellon.offset()") - - dx, dy = vect - - def offset_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(offset_geom(g)) - return new_obj - else: - try: - return affinity.translate(obj, xoff=dx, yoff=dy) - except AttributeError: - return obj - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.drills: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - - # Drills - for drill in self.drills: - drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # offset solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy) - slot['start'] = affinity.translate(slot['start'],xoff=dx, yoff=dy) - - # Recreate geometry - self.create_geometry() - self.app.proc_container.new_text = '' - - def mirror(self, axis, point): - """ - - :param axis: "X" or "Y" indicates around which axis to mirror. - :type axis: str - :param point: [x, y] point belonging to the mirror axis. - :type point: list - :return: None - """ - log.debug("camlib.Excellon.mirror()") - - px, py = point - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - def mirror_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(mirror_geom(g)) - return new_obj - else: - try: - return affinity.scale(obj, xscale, yscale, origin=(px, py)) - except AttributeError: - return obj - - # Modify data - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.drills: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - - # Drills - for drill in self.drills: - drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py)) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # mirror solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py)) - slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py)) - - # Recreate geometry - self.create_geometry() - self.app.proc_container.new_text = '' - - def skew(self, angle_x=None, angle_y=None, point=None): - """ - Shear/Skew the geometries of an object by angles along x and y dimensions. - Tool sizes, feedrates an Z-plane dimensions are untouched. - - Parameters - ---------- - xs, ys : float, float - The shear angle(s) for the x and y axes respectively. These can be - specified in either degrees (default) or radians by setting - use_radians=True. - - See shapely manual for more information: - http://toblerity.org/shapely/manual.html#affine-transformations - """ - log.debug("camlib.Excellon.skew()") - - if angle_x is None: - angle_x = 0.0 - - if angle_y is None: - angle_y = 0.0 - - def skew_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(skew_geom(g)) - return new_obj - else: - try: - return affinity.skew(obj, angle_x, angle_y, origin=(px, py)) - except AttributeError: - return obj - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.drills: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - - if point is None: - px, py = 0, 0 - - # Drills - for drill in self.drills: - drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, - origin=(px, py)) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # skew solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py)) - slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py)) - else: - px, py = point - # Drills - for drill in self.drills: - drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, - origin=(px, py)) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # skew solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = skew_geom( self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py)) - slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py)) - - self.create_geometry() - self.app.proc_container.new_text = '' - - def rotate(self, angle, point=None): - """ - Rotate the geometry of an object by an angle around the 'point' coordinates - :param angle: - :param point: tuple of coordinates (x, y) - :return: - """ - log.debug("camlib.Excellon.rotate()") - - def rotate_geom(obj, origin=None): - if type(obj) is list: - new_obj = [] - for g in obj: - new_obj.append(rotate_geom(g)) - return new_obj - else: - if origin: - try: - return affinity.rotate(obj, angle, origin=origin) - except AttributeError: - return obj - else: - try: - return affinity.rotate(obj, angle, origin=(px, py)) - except AttributeError: - return obj - - # variables to display the percentage of work done - self.geo_len = 0 - try: - for g in self.drills: - self.geo_len += 1 - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - - if point is None: - # Drills - for drill in self.drills: - drill['point'] = affinity.rotate(drill['point'], angle, origin='center') - - # rotate solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center') - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # Slots - for slot in self.slots: - slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center') - slot['start'] = affinity.rotate(slot['start'], angle, origin='center') - else: - px, py = point - # Drills - for drill in self.drills: - drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py)) - - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - # rotate solid_geometry - for tool in self.tools: - self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry']) - - # Slots - for slot in self.slots: - slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py)) - slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py)) - - self.create_geometry() - self.app.proc_container.new_text = '' - - class AttrDict(dict): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) diff --git a/flatcamEditors/FlatCAMExcEditor.py b/flatcamEditors/FlatCAMExcEditor.py index 5e46c2e0..5131d626 100644 --- a/flatcamEditors/FlatCAMExcEditor.py +++ b/flatcamEditors/FlatCAMExcEditor.py @@ -19,6 +19,7 @@ from rtree import index as rtindex from camlib import * from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor +from flatcamParsers.ParseExcellon import Excellon from copy import copy, deepcopy diff --git a/flatcamEditors/FlatCAMGrbEditor.py b/flatcamEditors/FlatCAMGrbEditor.py index 7c9218c1..b6732454 100644 --- a/flatcamEditors/FlatCAMGrbEditor.py +++ b/flatcamEditors/FlatCAMGrbEditor.py @@ -24,6 +24,7 @@ from camlib import * from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \ SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox from FlatCAMObj import FlatCAMGerber +from flatcamParsers.ParseGerber import Gerber from FlatCAMTool import FlatCAMTool from numpy.linalg import norm as numpy_norm diff --git a/flatcamParsers/ParseExcellon.py b/flatcamParsers/ParseExcellon.py new file mode 100644 index 00000000..c66140b7 --- /dev/null +++ b/flatcamParsers/ParseExcellon.py @@ -0,0 +1,1433 @@ +from camlib import * + +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class Excellon(Geometry): + """ + Here it is done all the Excellon parsing. + + *ATTRIBUTES* + + * ``tools`` (dict): The key is the tool name and the value is + a dictionary specifying the tool: + + ================ ==================================== + Key Value + ================ ==================================== + C Diameter of the tool + solid_geometry Geometry list for each tool + Others Not supported (Ignored). + ================ ==================================== + + * ``drills`` (list): Each is a dictionary: + + ================ ==================================== + Key Value + ================ ==================================== + point (Shapely.Point) Where to drill + tool (str) A key in ``tools`` + ================ ==================================== + + * ``slots`` (list): Each is a dictionary + + ================ ==================================== + Key Value + ================ ==================================== + start (Shapely.Point) Start point of the slot + stop (Shapely.Point) Stop point of the slot + tool (str) A key in ``tools`` + ================ ==================================== + """ + + defaults = { + "zeros": "L", + "excellon_format_upper_mm": '3', + "excellon_format_lower_mm": '3', + "excellon_format_upper_in": '2', + "excellon_format_lower_in": '4', + "excellon_units": 'INCH', + "geo_steps_per_circle": '64' + } + + def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None, + excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None, + geo_steps_per_circle=None): + """ + The constructor takes no parameters. + + :return: Excellon object. + :rtype: Excellon + """ + + if geo_steps_per_circle is None: + geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle']) + self.geo_steps_per_circle = int(geo_steps_per_circle) + + Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle)) + + # dictionary to store tools, see above for description + self.tools = {} + # list to store the drills, see above for description + self.drills = [] + + # self.slots (list) to store the slots; each is a dictionary + self.slots = [] + + self.source_file = '' + + # it serve to flag if a start routing or a stop routing was encountered + # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error + self.routing_flag = 1 + + self.match_routing_start = None + self.match_routing_stop = None + + self.num_tools = [] # List for keeping the tools sorted + self.index_per_tool = {} # Dictionary to store the indexed points for each tool + + # ## IN|MM -> Units are inherited from Geometry + # self.units = units + + # Trailing "T" or leading "L" (default) + # self.zeros = "T" + self.zeros = zeros or self.defaults["zeros"] + self.zeros_found = self.zeros + self.units_found = self.units + + # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be + # in another file like for PCB WIzard ECAD software + self.toolless_diam = 1.0 + # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter + self.diameterless = False + + # Excellon format + self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"] + self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"] + self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"] + self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"] + self.excellon_units = excellon_units or self.defaults["excellon_units"] + # detected Excellon format is stored here: + self.excellon_format = None + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from Geometry. + self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm', + 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots', + 'source_file'] + + # ### Patterns #### + # Regex basics: + # ^ - beginning + # $ - end + # *: 0 or more, +: 1 or more, ?: 0 or 1 + + # M48 - Beginning of Part Program Header + self.hbegin_re = re.compile(r'^M48$') + + # ;HEADER - Beginning of Allegro Program Header + self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)') + + # M95 or % - End of Part Program Header + # NOTE: % has different meaning in the body + self.hend_re = re.compile(r'^(?:M95|%)$') + + # FMAT Excellon format + # Ignored in the parser + # self.fmat_re = re.compile(r'^FMAT,([12])$') + + # Uunits and possible Excellon zeros and possible Excellon format + # INCH uses 6 digits + # METRIC uses 5/6 + self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$') + + # Tool definition/parameters (?= is look-ahead + # NOTE: This might be an overkill! + # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' + + # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + + # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + + # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') + self.toolset_re = re.compile(r'^T(\d+)(?=.*C,?(\d*\.?\d*))?' + + r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' + + r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' + + r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]') + + self.detect_gcode_re = re.compile(r'^G2([01])$') + + # Tool select + # Can have additional data after tool number but + # is ignored if present in the header. + # Warning: This will match toolset_re too. + # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))') + self.toolsel_re = re.compile(r'^T(\d+)') + + # Headerless toolset + # self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))') + self.toolset_hl_re = re.compile(r'^T(\d+)(?:.?C(\d+\.?\d*))?') + + # Comment + self.comm_re = re.compile(r'^;(.*)$') + + # Absolute/Incremental G90/G91 + self.absinc_re = re.compile(r'^G9([01])$') + + # Modes of operation + # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill + self.modes_re = re.compile(r'^G0([012345])') + + # Measuring mode + # 1-metric, 2-inch + self.meas_re = re.compile(r'^M7([12])$') + + # Coordinates + # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$') + # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$') + coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]' + self.coordsperiod_re = re.compile(coordsperiod_re_string) + + coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]' + self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string) + + # Slots parsing + slots_re_string = r'^([^G]+)G85(.*)$' + self.slots_re = re.compile(slots_re_string) + + # R - Repeat hole (# times, X offset, Y offset) + self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$') + + # Various stop/pause commands + self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))') + + # Allegro Excellon format support + self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))') + + # Altium Excellon format support + # it's a comment like this: ";FILE_FORMAT=2:5" + self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$') + + # Parse coordinates + self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)') + + # Repeating command + self.repeat_re = re.compile(r'R(\d+)') + + def parse_file(self, filename=None, file_obj=None): + """ + Reads the specified file as array of lines as + passes it to ``parse_lines()``. + + :param filename: The file to be read and parsed. + :type filename: str + :return: None + """ + if file_obj: + estr = file_obj + else: + if filename is None: + return "fail" + efile = open(filename, 'r') + estr = efile.readlines() + efile.close() + + try: + self.parse_lines(estr) + except: + return "fail" + + def parse_lines(self, elines): + """ + Main Excellon parser. + + :param elines: List of strings, each being a line of Excellon code. + :type elines: list + :return: None + """ + + # State variables + current_tool = "" + in_header = False + headerless = False + current_x = None + current_y = None + + slot_current_x = None + slot_current_y = None + + name_tool = 0 + allegro_warning = False + line_units_found = False + + repeating_x = 0 + repeating_y = 0 + repeat = 0 + + line_units = '' + + #### Parsing starts here ## ## + line_num = 0 # Line number + eline = "" + try: + for eline in elines: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + + line_num += 1 + # log.debug("%3d %s" % (line_num, str(eline))) + + self.source_file += eline + + # Cleanup lines + eline = eline.strip(' \r\n') + + # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe + # and we need to exit from here + if self.detect_gcode_re.search(eline): + log.warning("This is GCODE mark: %s" % eline) + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % + (_('This is GCODE mark'), eline)) + return + + # Header Begin (M48) # + if self.hbegin_re.search(eline): + in_header = True + headerless = False + log.warning("Found start of the header: %s" % eline) + continue + + # Allegro Header Begin (;HEADER) # + if self.allegro_hbegin_re.search(eline): + in_header = True + allegro_warning = True + log.warning("Found ALLEGRO start of the header: %s" % eline) + continue + + # Search for Header End # + # Since there might be comments in the header that include header end char (% or M95) + # we ignore the lines starting with ';' that contains such header end chars because it is not a + # real header end. + if self.comm_re.search(eline): + match = self.tool_units_re.search(eline) + if match: + if line_units_found is False: + line_units_found = True + line_units = match.group(3) + self.convert_units({"MILS": "IN", "MM": "MM"}[line_units]) + log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units) + + if match.group(2): + name_tool += 1 + if line_units == 'MILS': + spec = {"C": (float(match.group(2)) / 1000)} + self.tools[str(name_tool)] = spec + log.debug(" Tool definition: %s %s" % (name_tool, spec)) + else: + spec = {"C": float(match.group(2))} + self.tools[str(name_tool)] = spec + log.debug(" Tool definition: %s %s" % (name_tool, spec)) + spec['solid_geometry'] = [] + continue + # search for Altium Excellon Format / Sprint Layout who is included as a comment + match = self.altium_format.search(eline) + if match: + self.excellon_format_upper_mm = match.group(1) + self.excellon_format_lower_mm = match.group(2) + + self.excellon_format_upper_in = match.group(1) + self.excellon_format_lower_in = match.group(2) + log.warning("Altium Excellon format preset found in comments: %s:%s" % + (match.group(1), match.group(2))) + continue + else: + log.warning("Line ignored, it's a comment: %s" % eline) + else: + if self.hend_re.search(eline): + if in_header is False or bool(self.tools) is False: + log.warning("Found end of the header but there is no header: %s" % eline) + log.warning("The only useful data in header are tools, units and format.") + log.warning("Therefore we will create units and format based on defaults.") + headerless = True + try: + self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units]) + except Exception as e: + log.warning("Units could not be converted: %s" % str(e)) + + in_header = False + # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange + if allegro_warning is True: + name_tool = 0 + log.warning("Found end of the header: %s" % eline) + continue + + # ## Alternative units format M71/M72 + # Supposed to be just in the body (yes, the body) + # but some put it in the header (PADS for example). + # Will detect anywhere. Occurrence will change the + # object's units. + match = self.meas_re.match(eline) + if match: + # self.units = {"1": "MM", "2": "IN"}[match.group(1)] + + # Modified for issue #80 + self.convert_units({"1": "MM", "2": "IN"}[match.group(1)]) + log.debug(" Units: %s" % self.units) + if self.units == 'MM': + log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \ + ':' + str(self.excellon_format_lower_mm)) + else: + log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \ + ':' + str(self.excellon_format_lower_in)) + continue + + # ### Body #### + if not in_header: + + # ## Tool change ### + match = self.toolsel_re.search(eline) + if match: + current_tool = str(int(match.group(1))) + log.debug("Tool change: %s" % current_tool) + if bool(headerless): + match = self.toolset_hl_re.search(eline) + if match: + name = str(int(match.group(1))) + try: + diam = float(match.group(2)) + except: + # it's possible that tool definition has only tool number and no diameter info + # (those could be in another file like PCB Wizard do) + # then match.group(2) = None and float(None) will create the exception + # the bellow construction is so each tool will have a slightly different diameter + # starting with a default value, to allow Excellon editing after that + self.diameterless = True + self.app.inform.emit('[WARNING] %s%s %s' % + (_("No tool diameter info's. See shell.\n" + "A tool change event: T"), + str(current_tool), + _("was found but the Excellon file " + "have no informations regarding the tool " + "diameters therefore the application will try to load it " + "by using some 'fake' diameters.\n" + "The user needs to edit the resulting Excellon object and " + "change the diameters to reflect the real diameters.") + ) + ) + + if self.excellon_units == 'MM': + diam = self.toolless_diam + (int(current_tool) - 1) / 100 + else: + diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4 + + spec = {"C": diam, 'solid_geometry': []} + self.tools[name] = spec + log.debug("Tool definition out of header: %s %s" % (name, spec)) + + continue + + # ## Allegro Type Tool change ### + if allegro_warning is True: + match = self.absinc_re.search(eline) + match1 = self.stop_re.search(eline) + if match or match1: + name_tool += 1 + current_tool = str(name_tool) + log.debug("Tool change for Allegro type of Excellon: %s" % current_tool) + continue + + # ## Slots parsing for drilled slots (contain G85) + # a Excellon drilled slot line may look like this: + # X01125Y0022244G85Y0027756 + match = self.slots_re.search(eline) + if match: + # signal that there are milling slots operations + self.defaults['excellon_drills'] = False + + # the slot start coordinates group is to the left of G85 command (group(1) ) + # the slot stop coordinates group is to the right of G85 command (group(2) ) + start_coords_match = match.group(1) + stop_coords_match = match.group(2) + + # Slot coordinates without period # ## + # get the coordinates for slot start and for slot stop into variables + start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match) + stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match) + if start_coords_noperiod: + try: + slot_start_x = self.parse_number(start_coords_noperiod.group(1)) + slot_current_x = slot_start_x + except TypeError: + slot_start_x = slot_current_x + except: + return + + try: + slot_start_y = self.parse_number(start_coords_noperiod.group(2)) + slot_current_y = slot_start_y + except TypeError: + slot_start_y = slot_current_y + except: + return + + try: + slot_stop_x = self.parse_number(stop_coords_noperiod.group(1)) + slot_current_x = slot_stop_x + except TypeError: + slot_stop_x = slot_current_x + except: + return + + try: + slot_stop_y = self.parse_number(stop_coords_noperiod.group(2)) + slot_current_y = slot_stop_y + except TypeError: + slot_stop_y = slot_current_y + except: + return + + if (slot_start_x is None or slot_start_y is None or + slot_stop_x is None or slot_stop_y is None): + log.error("Slots are missing some or all coordinates.") + continue + + # we have a slot + log.debug('Parsed a slot with coordinates: ' + str([slot_start_x, + slot_start_y, slot_stop_x, + slot_stop_y])) + + # store current tool diameter as slot diameter + slot_dia = 0.05 + try: + slot_dia = float(self.tools[current_tool]['C']) + except Exception as e: + pass + log.debug( + 'Milling/Drilling slot with tool %s, diam=%f' % ( + current_tool, + slot_dia + ) + ) + + self.slots.append( + { + 'start': Point(slot_start_x, slot_start_y), + 'stop': Point(slot_stop_x, slot_stop_y), + 'tool': current_tool + } + ) + continue + + # Slot coordinates with period: Use literally. ### + # get the coordinates for slot start and for slot stop into variables + start_coords_period = self.coordsperiod_re.search(start_coords_match) + stop_coords_period = self.coordsperiod_re.search(stop_coords_match) + if start_coords_period: + + try: + slot_start_x = float(start_coords_period.group(1)) + slot_current_x = slot_start_x + except TypeError: + slot_start_x = slot_current_x + except: + return + + try: + slot_start_y = float(start_coords_period.group(2)) + slot_current_y = slot_start_y + except TypeError: + slot_start_y = slot_current_y + except: + return + + try: + slot_stop_x = float(stop_coords_period.group(1)) + slot_current_x = slot_stop_x + except TypeError: + slot_stop_x = slot_current_x + except: + return + + try: + slot_stop_y = float(stop_coords_period.group(2)) + slot_current_y = slot_stop_y + except TypeError: + slot_stop_y = slot_current_y + except: + return + + if (slot_start_x is None or slot_start_y is None or + slot_stop_x is None or slot_stop_y is None): + log.error("Slots are missing some or all coordinates.") + continue + + # we have a slot + log.debug('Parsed a slot with coordinates: ' + str([slot_start_x, + slot_start_y, slot_stop_x, + slot_stop_y])) + + # store current tool diameter as slot diameter + slot_dia = 0.05 + try: + slot_dia = float(self.tools[current_tool]['C']) + except Exception as e: + pass + log.debug( + 'Milling/Drilling slot with tool %s, diam=%f' % ( + current_tool, + slot_dia + ) + ) + + self.slots.append( + { + 'start': Point(slot_start_x, slot_start_y), + 'stop': Point(slot_stop_x, slot_stop_y), + 'tool': current_tool + } + ) + continue + + # ## Coordinates without period # ## + match = self.coordsnoperiod_re.search(eline) + if match: + matchr = self.repeat_re.search(eline) + if matchr: + repeat = int(matchr.group(1)) + + try: + x = self.parse_number(match.group(1)) + repeating_x = current_x + current_x = x + except TypeError: + x = current_x + repeating_x = 0 + except: + return + + try: + y = self.parse_number(match.group(2)) + repeating_y = current_y + current_y = y + except TypeError: + y = current_y + repeating_y = 0 + except: + return + + if x is None or y is None: + log.error("Missing coordinates") + continue + + # ## Excellon Routing parse + if len(re.findall("G00", eline)) > 0: + self.match_routing_start = 'G00' + + # signal that there are milling slots operations + self.defaults['excellon_drills'] = False + + self.routing_flag = 0 + slot_start_x = x + slot_start_y = y + continue + + if self.routing_flag == 0: + if len(re.findall("G01", eline)) > 0: + self.match_routing_stop = 'G01' + + # signal that there are milling slots operations + self.defaults['excellon_drills'] = False + + self.routing_flag = 1 + slot_stop_x = x + slot_stop_y = y + self.slots.append( + { + 'start': Point(slot_start_x, slot_start_y), + 'stop': Point(slot_stop_x, slot_stop_y), + 'tool': current_tool + } + ) + continue + + if self.match_routing_start is None and self.match_routing_stop is None: + if repeat == 0: + # signal that there are drill operations + self.defaults['excellon_drills'] = True + self.drills.append({'point': Point((x, y)), 'tool': current_tool}) + else: + coordx = x + coordy = y + while repeat > 0: + if repeating_x: + coordx = (repeat * x) + repeating_x + if repeating_y: + coordy = (repeat * y) + repeating_y + self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) + repeat -= 1 + repeating_x = repeating_y = 0 + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) + continue + + # ## Coordinates with period: Use literally. # ## + match = self.coordsperiod_re.search(eline) + if match: + matchr = self.repeat_re.search(eline) + if matchr: + repeat = int(matchr.group(1)) + + if match: + # signal that there are drill operations + self.defaults['excellon_drills'] = True + try: + x = float(match.group(1)) + repeating_x = current_x + current_x = x + except TypeError: + x = current_x + repeating_x = 0 + + try: + y = float(match.group(2)) + repeating_y = current_y + current_y = y + except TypeError: + y = current_y + repeating_y = 0 + + if x is None or y is None: + log.error("Missing coordinates") + continue + + # ## Excellon Routing parse + if len(re.findall("G00", eline)) > 0: + self.match_routing_start = 'G00' + + # signal that there are milling slots operations + self.defaults['excellon_drills'] = False + + self.routing_flag = 0 + slot_start_x = x + slot_start_y = y + continue + + if self.routing_flag == 0: + if len(re.findall("G01", eline)) > 0: + self.match_routing_stop = 'G01' + + # signal that there are milling slots operations + self.defaults['excellon_drills'] = False + + self.routing_flag = 1 + slot_stop_x = x + slot_stop_y = y + self.slots.append( + { + 'start': Point(slot_start_x, slot_start_y), + 'stop': Point(slot_stop_x, slot_stop_y), + 'tool': current_tool + } + ) + continue + + if self.match_routing_start is None and self.match_routing_stop is None: + # signal that there are drill operations + if repeat == 0: + # signal that there are drill operations + self.defaults['excellon_drills'] = True + self.drills.append({'point': Point((x, y)), 'tool': current_tool}) + else: + coordx = x + coordy = y + while repeat > 0: + if repeating_x: + coordx = (repeat * x) + repeating_x + if repeating_y: + coordy = (repeat * y) + repeating_y + self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) + repeat -= 1 + repeating_x = repeating_y = 0 + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) + continue + + # ### Header #### + if in_header: + + # ## Tool definitions # ## + match = self.toolset_re.search(eline) + if match: + name = str(int(match.group(1))) + spec = {"C": float(match.group(2)), 'solid_geometry': []} + self.tools[name] = spec + log.debug(" Tool definition: %s %s" % (name, spec)) + continue + + # ## Units and number format # ## + match = self.units_re.match(eline) + if match: + self.units_found = match.group(1) + self.zeros = match.group(2) # "T" or "L". Might be empty + self.excellon_format = match.group(3) + if self.excellon_format: + upper = len(self.excellon_format.partition('.')[0]) + lower = len(self.excellon_format.partition('.')[2]) + if self.units == 'MM': + self.excellon_format_upper_mm = upper + self.excellon_format_lower_mm = lower + else: + self.excellon_format_upper_in = upper + self.excellon_format_lower_in = lower + + # Modified for issue #80 + self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found]) + # log.warning(" Units/Format: %s %s" % (self.units, self.zeros)) + log.warning("Units: %s" % self.units) + if self.units == 'MM': + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + + ':' + str(self.excellon_format_lower_mm)) + else: + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + + ':' + str(self.excellon_format_lower_in)) + log.warning("Type of zeros found inline: %s" % self.zeros) + continue + + # Search for units type again it might be alone on the line + if "INCH" in eline: + line_units = "INCH" + # Modified for issue #80 + self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units]) + log.warning("Type of UNITS found inline: %s" % line_units) + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + + ':' + str(self.excellon_format_lower_in)) + # TODO: not working + # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline)) + continue + elif "METRIC" in eline: + line_units = "METRIC" + # Modified for issue #80 + self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units]) + log.warning("Type of UNITS found inline: %s" % line_units) + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + + ':' + str(self.excellon_format_lower_mm)) + # TODO: not working + # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline)) + continue + + # Search for zeros type again because it might be alone on the line + match = re.search(r'[LT]Z', eline) + if match: + self.zeros = match.group() + log.warning("Type of zeros found: %s" % self.zeros) + continue + + # ## Units and number format outside header# ## + match = self.units_re.match(eline) + if match: + self.units_found = match.group(1) + self.zeros = match.group(2) # "T" or "L". Might be empty + self.excellon_format = match.group(3) + if self.excellon_format: + upper = len(self.excellon_format.partition('.')[0]) + lower = len(self.excellon_format.partition('.')[2]) + if self.units == 'MM': + self.excellon_format_upper_mm = upper + self.excellon_format_lower_mm = lower + else: + self.excellon_format_upper_in = upper + self.excellon_format_lower_in = lower + + # Modified for issue #80 + self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found]) + # log.warning(" Units/Format: %s %s" % (self.units, self.zeros)) + log.warning("Units: %s" % self.units) + if self.units == 'MM': + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) + + ':' + str(self.excellon_format_lower_mm)) + else: + log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) + + ':' + str(self.excellon_format_lower_in)) + log.warning("Type of zeros found outside header, inline: %s" % self.zeros) + + log.warning("UNITS found outside header") + continue + + log.warning("Line ignored: %s" % eline) + + # make sure that since we are in headerless mode, we convert the tools only after the file parsing + # is finished since the tools definitions are spread in the Excellon body. We use as units the value + # from self.defaults['excellon_units'] + log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) + except Exception as e: + log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline)) + msg = '[ERROR_NOTCL] %s' % \ + _("An internal error has ocurred. See shell.\n") + msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format( + e_code='[ERROR]', + l_nr=line_num, + line=eline) + msg += traceback.format_exc() + self.app.inform.emit(msg) + + return "fail" + + def parse_number(self, number_str): + """ + Parses coordinate numbers without period. + + :param number_str: String representing the numerical value. + :type number_str: str + :return: Floating point representation of the number + :rtype: float + """ + + match = self.leadingzeros_re.search(number_str) + nr_length = len(match.group(1)) + len(match.group(2)) + try: + if self.zeros == "L" or self.zeros == "LZ": # Leading + # With leading zeros, when you type in a coordinate, + # the leading zeros must always be included. Trailing zeros + # are unneeded and may be left off. The CNC-7 will automatically add them. + # r'^[-\+]?(0*)(\d*)' + # 6 digits are divided by 10^4 + # If less than size digits, they are automatically added, + # 5 digits then are divided by 10^3 and so on. + + if self.units.lower() == "in": + result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in))) + else: + result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm))) + return result + else: # Trailing + # You must show all zeros to the right of the number and can omit + # all zeros to the left of the number. The CNC-7 will count the number + # of digits you typed and automatically fill in the missing zeros. + # ## flatCAM expects 6digits + # flatCAM expects the number of digits entered into the defaults + + if self.units.lower() == "in": # Inches is 00.0000 + result = float(number_str) / (10 ** (float(self.excellon_format_lower_in))) + else: # Metric is 000.000 + result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm))) + return result + except Exception as e: + log.error("Aborted. Operation could not be completed due of %s" % str(e)) + return + + def create_geometry(self): + """ + Creates circles of the tool diameter at every point + specified in ``self.drills``. Also creates geometries (polygons) + for the slots as specified in ``self.slots`` + All the resulting geometry is stored into self.solid_geometry list. + The list self.solid_geometry has 2 elements: first is a dict with the drills geometry, + and second element is another similar dict that contain the slots geometry. + + Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries + ================ ==================================== + Key Value + ================ ==================================== + tool_diameter list of (Shapely.Point) Where to drill + ================ ==================================== + + :return: None + """ + self.solid_geometry = [] + try: + # clear the solid_geometry in self.tools + for tool in self.tools: + try: + self.tools[tool]['solid_geometry'][:] = [] + except KeyError: + self.tools[tool]['solid_geometry'] = [] + + for drill in self.drills: + # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0) + if drill['tool'] is '': + self.app.inform.emit('[WARNING] %s' % + _("Excellon.create_geometry() -> a drill location was skipped " + "due of not having a tool associated.\n" + "Check the resulting GCode.")) + log.debug("Excellon.create_geometry() -> a drill location was skipped " + "due of not having a tool associated") + continue + tooldia = self.tools[drill['tool']]['C'] + poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4)) + self.solid_geometry.append(poly) + self.tools[drill['tool']]['solid_geometry'].append(poly) + + for slot in self.slots: + slot_tooldia = self.tools[slot['tool']]['C'] + start = slot['start'] + stop = slot['stop'] + + lines_string = LineString([start, stop]) + poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4)) + self.solid_geometry.append(poly) + self.tools[slot['tool']]['solid_geometry'].append(poly) + + except Exception as e: + log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e)) + return "fail" + + # drill_geometry = {} + # slot_geometry = {} + # + # def insertIntoDataStruct(dia, drill_geo, aDict): + # if not dia in aDict: + # aDict[dia] = [drill_geo] + # else: + # aDict[dia].append(drill_geo) + # + # for tool in self.tools: + # tooldia = self.tools[tool]['C'] + # for drill in self.drills: + # if drill['tool'] == tool: + # poly = drill['point'].buffer(tooldia / 2.0) + # insertIntoDataStruct(tooldia, poly, drill_geometry) + # + # for tool in self.tools: + # slot_tooldia = self.tools[tool]['C'] + # for slot in self.slots: + # if slot['tool'] == tool: + # start = slot['start'] + # stop = slot['stop'] + # lines_string = LineString([start, stop]) + # poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle) + # insertIntoDataStruct(slot_tooldia, poly, drill_geometry) + # + # self.solid_geometry = [drill_geometry, slot_geometry] + + def bounds(self): + """ + Returns coordinates of rectangular bounds + of Excellon geometry: (xmin, ymin, xmax, ymax). + """ + # fixed issue of getting bounds only for one level lists of objects + # now it can get bounds for nested lists of objects + + log.debug("camlib.Excellon.bounds()") + if self.solid_geometry is None: + log.debug("solid_geometry is None") + return 0, 0, 0, 0 + + def bounds_rec(obj): + if type(obj) is list: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in obj: + if type(k) is dict: + for key in k: + minx_, miny_, maxx_, maxy_ = bounds_rec(k[key]) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + else: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + return obj.bounds + + minx_list = [] + miny_list = [] + maxx_list = [] + maxy_list = [] + + for tool in self.tools: + minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry']) + minx_list.append(minx) + miny_list.append(miny) + maxx_list.append(maxx) + maxy_list.append(maxy) + + return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)) + + def convert_units(self, units): + """ + This function first convert to the the units found in the Excellon file but it converts tools that + are not there yet so it has no effect other than it signal that the units are the ones in the file. + + On object creation, in new_object(), true conversion is done because this is done at the end of the + Excellon file parsing, the tools are inside and self.tools is really converted from the units found + inside the file to the FlatCAM units. + + Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file + will have detected the units before the tools are parsed and stored in self.tools + :param units: + :type str: IN or MM + :return: + """ + log.debug("camlib.Excellon.convert_units()") + + factor = Geometry.convert_units(self, units) + + # Tools + for tname in self.tools: + self.tools[tname]["C"] *= factor + + self.create_geometry() + + return factor + + def scale(self, xfactor, yfactor=None, point=None): + """ + Scales geometry on the XY plane in the object by a given factor. + Tool sizes, feedrates an Z-plane dimensions are untouched. + + :param factor: Number by which to scale the object. + :type factor: float + :return: None + :rtype: NOne + """ + log.debug("camlib.Excellon.scale()") + + if yfactor is None: + yfactor = xfactor + + if point is None: + px = 0 + py = 0 + else: + px, py = point + + def scale_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(scale_geom(g)) + return new_obj + else: + try: + return affinity.scale(obj, xfactor, yfactor, origin=(px, py)) + except AttributeError: + return obj + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.drills: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + # Drills + for drill in self.drills: + drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py)) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # scale solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py)) + slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py)) + + self.create_geometry() + self.app.proc_container.new_text = '' + + def offset(self, vect): + """ + Offsets geometry on the XY plane in the object by a given vector. + + :param vect: (x, y) offset vector. + :type vect: tuple + :return: None + """ + log.debug("camlib.Excellon.offset()") + + dx, dy = vect + + def offset_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(offset_geom(g)) + return new_obj + else: + try: + return affinity.translate(obj, xoff=dx, yoff=dy) + except AttributeError: + return obj + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.drills: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + # Drills + for drill in self.drills: + drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # offset solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy) + slot['start'] = affinity.translate(slot['start'], xoff=dx, yoff=dy) + + # Recreate geometry + self.create_geometry() + self.app.proc_container.new_text = '' + + def mirror(self, axis, point): + """ + + :param axis: "X" or "Y" indicates around which axis to mirror. + :type axis: str + :param point: [x, y] point belonging to the mirror axis. + :type point: list + :return: None + """ + log.debug("camlib.Excellon.mirror()") + + px, py = point + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + def mirror_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(mirror_geom(g)) + return new_obj + else: + try: + return affinity.scale(obj, xscale, yscale, origin=(px, py)) + except AttributeError: + return obj + + # Modify data + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.drills: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + # Drills + for drill in self.drills: + drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py)) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # mirror solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py)) + slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py)) + + # Recreate geometry + self.create_geometry() + self.app.proc_container.new_text = '' + + def skew(self, angle_x=None, angle_y=None, point=None): + """ + Shear/Skew the geometries of an object by angles along x and y dimensions. + Tool sizes, feedrates an Z-plane dimensions are untouched. + + Parameters + ---------- + xs, ys : float, float + The shear angle(s) for the x and y axes respectively. These can be + specified in either degrees (default) or radians by setting + use_radians=True. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + log.debug("camlib.Excellon.skew()") + + if angle_x is None: + angle_x = 0.0 + + if angle_y is None: + angle_y = 0.0 + + def skew_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(skew_geom(g)) + return new_obj + else: + try: + return affinity.skew(obj, angle_x, angle_y, origin=(px, py)) + except AttributeError: + return obj + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.drills: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + if point is None: + px, py = 0, 0 + + # Drills + for drill in self.drills: + drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, + origin=(px, py)) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # skew solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py)) + slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py)) + else: + px, py = point + # Drills + for drill in self.drills: + drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, + origin=(px, py)) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # skew solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py)) + slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py)) + + self.create_geometry() + self.app.proc_container.new_text = '' + + def rotate(self, angle, point=None): + """ + Rotate the geometry of an object by an angle around the 'point' coordinates + :param angle: + :param point: tuple of coordinates (x, y) + :return: + """ + log.debug("camlib.Excellon.rotate()") + + def rotate_geom(obj, origin=None): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(rotate_geom(g)) + return new_obj + else: + if origin: + try: + return affinity.rotate(obj, angle, origin=origin) + except AttributeError: + return obj + else: + try: + return affinity.rotate(obj, angle, origin=(px, py)) + except AttributeError: + return obj + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.drills: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + if point is None: + # Drills + for drill in self.drills: + drill['point'] = affinity.rotate(drill['point'], angle, origin='center') + + # rotate solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center') + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # Slots + for slot in self.slots: + slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center') + slot['start'] = affinity.rotate(slot['start'], angle, origin='center') + else: + px, py = point + # Drills + for drill in self.drills: + drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py)) + + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + # rotate solid_geometry + for tool in self.tools: + self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry']) + + # Slots + for slot in self.slots: + slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py)) + slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py)) + + self.create_geometry() + self.app.proc_container.new_text = '' \ No newline at end of file diff --git a/flatcamParsers/ParseGerber.py b/flatcamParsers/ParseGerber.py new file mode 100644 index 00000000..3402d1f1 --- /dev/null +++ b/flatcamParsers/ParseGerber.py @@ -0,0 +1,1976 @@ + +from camlib import * + +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class Gerber(Geometry): + """ + Here it is done all the Gerber parsing. + + **ATTRIBUTES** + + * ``apertures`` (dict): The keys are names/identifiers of each aperture. + The values are dictionaries key/value pairs which describe the aperture. The + type key is always present and the rest depend on the key: + + +-----------+-----------------------------------+ + | Key | Value | + +===========+===================================+ + | type | (str) "C", "R", "O", "P", or "AP" | + +-----------+-----------------------------------+ + | others | Depend on ``type`` | + +-----------+-----------------------------------+ + | solid_geometry | (list) | + +-----------+-----------------------------------+ + * ``aperture_macros`` (dictionary): Are predefined geometrical structures + that can be instantiated with different parameters in an aperture + definition. See ``apertures`` above. The key is the name of the macro, + and the macro itself, the value, is a ``Aperture_Macro`` object. + + * ``flash_geometry`` (list): List of (Shapely) geometric object resulting + from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``. + + * ``buffered_paths`` (list): List of (Shapely) polygons resulting from + *buffering* (or thickening) the ``paths`` with the aperture. These are + generated from ``paths`` in ``buffer_paths()``. + + **USAGE**:: + + g = Gerber() + g.parse_file(filename) + g.create_geometry() + do_something(s.solid_geometry) + + """ + + # defaults = { + # "steps_per_circle": 128, + # "use_buffer_for_union": True + # } + + def __init__(self, steps_per_circle=None): + """ + The constructor takes no parameters. Use ``gerber.parse_files()`` + or ``gerber.parse_lines()`` to populate the object from Gerber source. + + :return: Gerber object + :rtype: Gerber + """ + + # How to approximate a circle with lines. + self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"]) + + # Initialize parent + Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) + + # Number format + self.int_digits = 3 + """Number of integer digits in Gerber numbers. Used during parsing.""" + + self.frac_digits = 4 + """Number of fraction digits in Gerber numbers. Used during parsing.""" + + self.gerber_zeros = self.app.defaults['gerber_def_zeros'] + """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing. + """ + + # ## Gerber elements # ## + ''' + apertures = { + 'id':{ + 'type':string, + 'size':float, + 'width':float, + 'height':float, + 'geometry': [], + } + } + apertures['geometry'] list elements are dicts + dict = { + 'solid': [], + 'follow': [], + 'clear': [] + } + ''' + + # store the file units here: + self.gerber_units = self.app.defaults['gerber_def_units'] + + # aperture storage + self.apertures = {} + + # Aperture Macros + self.aperture_macros = {} + + # will store the Gerber geometry's as solids + self.solid_geometry = Polygon() + + # will store the Gerber geometry's as paths + self.follow_geometry = [] + + # made True when the LPC command is encountered in Gerber parsing + # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict + self.is_lpc = False + + self.source_file = '' + + # Attributes to be included in serialization + # Always append to it because it carries contents + # from Geometry. + self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', + 'aperture_macros', 'solid_geometry', 'source_file'] + + # ### Parser patterns ## ## + # FS - Format Specification + # The format of X and Y must be the same! + # L-omit leading zeros, T-omit trailing zeros, D-no zero supression + # A-absolute notation, I-incremental notation + self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\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\*%$') + + # Mode (IN/MM) + self.mode_re = re.compile(r'^%?MO(IN|MM)\*%?$') + + # Comment G04|G4 + self.comm_re = re.compile(r'^G0?4(.*)$') + + # AD - Aperture definition + # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+} + # NOTE: Adding "-" to support output from Upverter. + self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$') + + # AM - Aperture Macro + # Beginning of macro (Ends with *%): + # self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*') + + # Tool change + # May begin with G54 but that is deprecated + self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$') + + # G01... - Linear interpolation plus flashes with coordinates + # Operation code (D0x) missing is deprecated... oh well I will support it. + self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$') + + # Operation code alone, usually just D03 (Flash) + self.opcode_re = re.compile(r'^D0?([123])\*$') + + # G02/3... - Circular interpolation with coordinates + # 2-clockwise, 3-counterclockwise + # Operation code (D0x) missing is deprecated... oh well I will support it. + # Optional start with G02 or G03, optional end with D01 or D02 with + # optional coordinates but at least one in any order. + 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]).*\*$') + + # Region mode on + # In region mode, D01 starts a region + # and D02 ends it. A new region can be started again + # with D01. All contours must be closed before + # D02 or G37. + self.regionon_re = re.compile(r'^G36\*$') + + # Region mode off + # Will end a region and come off region mode. + # All contours must be closed before D02 or G37. + self.regionoff_re = re.compile(r'^G37\*$') + + # End of file + self.eof_re = re.compile(r'^M02\*') + + # IP - Image polarity + self.pol_re = re.compile(r'^%?IP(POS|NEG)\*%?$') + + # LP - Level polarity + self.lpol_re = re.compile(r'^%LP([DC])\*%$') + + # Units (OBSOLETE) + self.units_re = re.compile(r'^G7([01])\*$') + + # Absolute/Relative G90/1 (OBSOLETE) + self.absrel_re = re.compile(r'^G9([01])\*$') + + # Aperture macros + self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$') + self.am2_re = re.compile(r'(.*)%$') + + self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"] + + def aperture_parse(self, apertureId, apertureType, apParameters): + """ + Parse gerber aperture definition into dictionary of apertures. + The following kinds and their attributes are supported: + + * *Circular (C)*: size (float) + * *Rectangle (R)*: width (float), height (float) + * *Obround (O)*: width (float), height (float). + * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)] + * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list) + + :param apertureId: Id of the aperture being defined. + :param apertureType: Type of the aperture. + :param apParameters: Parameters of the aperture. + :type apertureId: str + :type apertureType: str + :type apParameters: str + :return: Identifier of the aperture. + :rtype: str + """ + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + + # Found some Gerber with a leading zero in the aperture id and the + # referenced it without the zero, so this is a hack to handle that. + apid = str(int(apertureId)) + + try: # Could be empty for aperture macros + paramList = apParameters.split('X') + except: + paramList = None + + if apertureType == "C": # Circle, example: %ADD11C,0.1*% + self.apertures[apid] = {"type": "C", + "size": float(paramList[0])} + return apid + + if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*% + self.apertures[apid] = {"type": "R", + "width": float(paramList[0]), + "height": float(paramList[1]), + "size": sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)} # Hack + return apid + + if apertureType == "O": # Obround + self.apertures[apid] = {"type": "O", + "width": float(paramList[0]), + "height": float(paramList[1]), + "size": sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)} # Hack + return apid + + if apertureType == "P": # Polygon (regular) + self.apertures[apid] = {"type": "P", + "diam": float(paramList[0]), + "nVertices": int(paramList[1]), + "size": float(paramList[0])} # Hack + if len(paramList) >= 3: + self.apertures[apid]["rotation"] = float(paramList[2]) + return apid + + if apertureType in self.aperture_macros: + self.apertures[apid] = {"type": "AM", + "macro": self.aperture_macros[apertureType], + "modifiers": paramList} + return apid + + log.warning("Aperture not implemented: %s" % str(apertureType)) + return None + + def parse_file(self, filename, follow=False): + """ + 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. + :type filename: str + :param follow: If true, will not create polygons, just lines + following the gerber path. + :type follow: bool + :return: None + """ + + with open(filename, 'r') as gfile: + + def line_generator(): + for line in gfile: + line = line.strip(' \r\n') + while len(line) > 0: + + # If ends with '%' leave as is. + if line[-1] == '%': + yield line + break + + # Split after '*' if any. + starpos = line.find('*') + if starpos > -1: + cleanline = line[:starpos + 1] + yield cleanline + line = line[starpos + 1:] + + # Otherwise leave as is. + else: + # yield clean line + yield line + break + + processed_lines = list(line_generator()) + self.parse_lines(processed_lines) + + # @profile + def parse_lines(self, glines): + """ + Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, + ``self.flashes``, ``self.regions`` and ``self.units``. + + :param glines: Gerber 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 = [] + + # 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 + + # Current coordinates + current_x = None + current_y = None + previous_x = None + previous_y = None + + current_d = None + + # 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 + + # ### 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"))) + try: + for gline in glines: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + + 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.comm_re.search(gline) + if match: + continue + + # Polarity change ###### ## + # Example: %LPD*% or %LPC*% + # If polarity changes, creates geometry from current + # buffer, then adds or subtracts accordingly. + match = self.lpol_re.search(gline) + if match: + new_polarity = match.group(1) + # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer)) + self.is_lpc = True if new_polarity == 'C' else False + if len(path) > 1 and current_polarity != new_polarity: + + # finish the current path and add it to the storage + # --- Buffered ---- + width = self.apertures[last_path_aperture]["size"] + + geo_dict = dict() + geo_f = LineString(path) + if not geo_f.is_empty: + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + + geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + 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 + + 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)) + + path = [path[-1]] + + # --- Apply buffer --- + # If added for testing of bug #83 + # TODO: Remove when bug fixed + if len(poly_buffer) > 0: + if current_polarity == 'D': + # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer)) + self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) + + else: + # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer)) + self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) + + # follow_buffer = [] + poly_buffer = [] + + current_polarity = new_polarity + continue + + # ############################################################# ## + # Number format ############################################### ## + # Example: %FSLAX24Y24*% + # ############################################################# ## + # TODO: This is ignoring most of the format. Implement the rest. + match = self.fmt_re.search(gline) + if match: + absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)] + if match.group(1) is not None: + self.gerber_zeros = match.group(1) + self.int_digits = int(match.group(3)) + self.frac_digits = int(match.group(4)) + log.debug("Gerber format found. (%s) " % str(gline)) + + log.debug( + "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " + "D-no zero supression)" % self.gerber_zeros) + log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) + continue + + # ## Mode (IN/MM) + # Example: %MOIN*% + match = self.mode_re.search(gline) + if match: + self.gerber_units = match.group(1) + log.debug("Gerber units found = %s" % self.gerber_units) + # Changed for issue #80 + self.convert_units(match.group(1)) + continue + + # ############################################################# ## + # Combined Number format and Mode --- Allegro does this ####### ## + # ############################################################# ## + match = self.fmt_re_alt.search(gline) + if match: + absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)] + if match.group(1) is not None: + self.gerber_zeros = match.group(1) + self.int_digits = int(match.group(3)) + self.frac_digits = int(match.group(4)) + log.debug("Gerber format found. (%s) " % str(gline)) + log.debug( + "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " + "D-no zero suppression)" % self.gerber_zeros) + log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) + + self.gerber_units = match.group(5) + log.debug("Gerber units found = %s" % self.gerber_units) + # Changed for issue #80 + self.convert_units(match.group(5)) + continue + + # ############################################################# ## + # Search for OrCAD way for having Number format + # ############################################################# ## + match = self.fmt_re_orcad.search(gline) + if match: + if match.group(1) is not None: + if match.group(1) == 'G74': + quadrant_mode = 'SINGLE' + elif match.group(1) == 'G75': + quadrant_mode = 'MULTI' + absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)] + if match.group(2) is not None: + self.gerber_zeros = match.group(2) + + self.int_digits = int(match.group(4)) + self.frac_digits = int(match.group(5)) + log.debug("Gerber format found. (%s) " % str(gline)) + log.debug( + "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, " + "D-no zerosuppressionn)" % self.gerber_zeros) + log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute) + + self.gerber_units = match.group(1) + log.debug("Gerber units found = %s" % self.gerber_units) + # Changed for issue #80 + self.convert_units(match.group(5)) + continue + + # ############################################################# ## + # Units (G70/1) OBSOLETE + # ############################################################# ## + match = self.units_re.search(gline) + if match: + obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)] + log.warning("Gerber obsolete units found = %s" % obs_gerber_units) + # Changed for issue #80 + self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)]) + continue + + # ############################################################# ## + # 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 + + # ############################################################# ## + # Aperture Macros ##################################### ## + # Having this at the beginning will slow things down + # but macros can have complicated statements than could + # be caught by other patterns. + # ############################################################# ## + if current_macro is None: # No macro started yet + match = self.am1_re.search(gline) + # Start macro if match, else not an AM, carry on. + if match: + log.debug("Starting macro. Line %d: %s" % (line_num, gline)) + current_macro = match.group(1) + self.aperture_macros[current_macro] = ApertureMacro(name=current_macro) + if match.group(2): # Append + self.aperture_macros[current_macro].append(match.group(2)) + if match.group(3): # Finish macro + # self.aperture_macros[current_macro].parse_content() + current_macro = None + log.debug("Macro complete in 1 line.") + continue + else: # Continue macro + log.debug("Continuing macro. Line %d." % line_num) + match = self.am2_re.search(gline) + if match: # Finish macro + log.debug("End of macro. Line %d." % line_num) + self.aperture_macros[current_macro].append(match.group(1)) + # self.aperture_macros[current_macro].parse_content() + current_macro = None + else: # Append + self.aperture_macros[current_macro].append(gline) + continue + + # ## Aperture definitions %ADD... + match = self.ad_re.search(gline) + if match: + # log.info("Found aperture definition. Line %d: %s" % (line_num, gline)) + self.aperture_parse(match.group(1), match.group(2), match.group(3)) + continue + + # ############################################################# ## + # Operation code alone ###################### ## + # Operation code alone, usually just D03 (Flash) + # self.opcode_re = re.compile(r'^D0?([123])\*$') + # ############################################################# ## + match = self.opcode_re.search(gline) + if match: + current_operation_code = int(match.group(1)) + current_d = current_operation_code + + if current_operation_code == 3: + + # --- Buffered --- + try: + log.debug("Bare op-code %d." % current_operation_code) + geo_dict = dict() + flash = self.create_flash_geometry( + Point(current_x, current_y), self.apertures[current_aperture], + self.steps_per_circle) + + geo_dict['follow'] = Point([current_x, current_y]) + + if not flash.is_empty: + if self.app.defaults['gerber_simplification']: + poly_buffer.append(flash.simplify(s_tol)) + else: + poly_buffer.append(flash) + if self.is_lpc is True: + geo_dict['clear'] = flash + else: + geo_dict['solid'] = flash + + 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 IndexError: + log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline)) + + continue + + # ############################################################# ## + # Tool/aperture change + # Example: D12* + # ############################################################# ## + match = self.tool_re.search(gline) + if match: + current_aperture = match.group(1) + # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture)) + + # If the aperture value is zero then make it something quite small but with a non-zero value + # so it can be processed by FlatCAM. + # But first test to see if the aperture type is "aperture macro". In that case + # we should not test for "size" key as it does not exist in this case. + if self.apertures[current_aperture]["type"] is not "AM": + if self.apertures[current_aperture]["size"] == 0: + self.apertures[current_aperture]["size"] = 1e-12 + # log.debug(self.apertures[current_aperture]) + + # Take care of the current path with the previous tool + if len(path) > 1: + if self.apertures[last_path_aperture]["type"] == 'R': + # do nothing because 'R' type moving aperture is none at once + pass + else: + geo_dict = dict() + geo_f = LineString(path) + if not geo_f.is_empty: + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + + # --- Buffered ---- + width = self.apertures[last_path_aperture]["size"] + geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + 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 + + 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)) + + path = [path[-1]] + + continue + + # ############################################################# ## + # G36* - Begin region + # ############################################################# ## + if self.regionon_re.search(gline): + if len(path) > 1: + # Take care of what is left in the path + + geo_dict = dict() + geo_f = LineString(path) + if not geo_f.is_empty: + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + + # --- Buffered ---- + width = self.apertures[last_path_aperture]["size"] + geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + 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 + + 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)) + + path = [path[-1]] + + making_region = True + continue + + # ############################################################# ## + # G37* - End region + # ############################################################# ## + if self.regionoff_re.search(gline): + making_region = False + + if '0' not in self.apertures: + self.apertures['0'] = {} + self.apertures['0']['type'] = 'REG' + self.apertures['0']['size'] = 0.0 + self.apertures['0']['geometry'] = [] + + # if D02 happened before G37 we now have a path with 1 element only; we have to add the current + # geo to the poly_buffer otherwise we loose it + if current_operation_code == 2: + if len(path) == 1: + # this means that the geometry was prepared previously and we just need to add it + geo_dict = dict() + if geo_f: + if not geo_f.is_empty: + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + if geo_s: + 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 + + if geo_s or geo_f: + self.apertures['0']['geometry'].append(deepcopy(geo_dict)) + + path = [[current_x, current_y]] # Start new path + + # Only one path defines region? + # This can happen if D02 happened before G37 and + # is not and error. + if len(path) < 3: + # print "ERROR: Path contains less than 3 points:" + # path = [[current_x, current_y]] + continue + + # For regions we may ignore an aperture that is None + + # --- Buffered --- + geo_dict = dict() + region_f = Polygon(path).exterior + if not region_f.is_empty: + follow_buffer.append(region_f) + geo_dict['follow'] = region_f + + region_s = Polygon(path) + if not region_s.is_valid: + region_s = region_s.buffer(0, int(self.steps_per_circle / 4)) + + if not region_s.is_empty: + if self.app.defaults['gerber_simplification']: + poly_buffer.append(region_s.simplify(s_tol)) + else: + poly_buffer.append(region_s) + if self.is_lpc is True: + geo_dict['clear'] = region_s + else: + geo_dict['solid'] = region_s + + if not region_s.is_empty or not region_f.is_empty: + self.apertures['0']['geometry'].append(deepcopy(geo_dict)) + + path = [[current_x, current_y]] # Start new path + continue + + # ## G01/2/3* - Interpolation mode change + # Can occur along with coordinates and operation code but + # sometimes by itself (handled here). + # Example: G01* + match = self.interp_re.search(gline) + if match: + current_interpolation_mode = int(match.group(1)) + 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) + if match: + # Dxx alone? + # if match.group(1) is None and match.group(2) is None and match.group(3) is None: + # try: + # current_operation_code = int(match.group(4)) + # except: + # pass # A line with just * will match too. + # continue + # NOTE: Letting it continue allows it to react to the + # operation code. + + # Parse coordinates + if match.group(2) is not None: + linear_x = parse_gerber_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_gerber_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_f = 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 !!!")) + + # Flash + # Not allowed in region mode. + elif current_operation_code == 3: + + # Create path draw so far. + if len(path) > 1: + # --- Buffered ---- + geo_dict = dict() + + # this treats the case when we are storing geometry as paths + geo_f = LineString(path) + if not geo_f.is_empty: + try: + if self.apertures[last_path_aperture]["type"] != 'R': + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + except Exception as e: + log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e)) + follow_buffer.append(geo_f) + geo_dict['follow'] = geo_f + + # this treats the case when we are storing geometry as solids + width = self.apertures[last_path_aperture]["size"] + geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + if not geo_s.is_empty: + try: + if self.apertures[last_path_aperture]["type"] != 'R': + 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: + 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)) + + # Reset path starting point + path = [[linear_x, linear_y]] + + # --- BUFFERED --- + # Draw the flash + # this treats the case when we are storing geometry as paths + geo_dict = dict() + geo_flash = Point([linear_x, linear_y]) + follow_buffer.append(geo_flash) + geo_dict['follow'] = geo_flash + + # this treats the case when we are storing geometry as solids + flash = self.create_flash_geometry( + Point([linear_x, linear_y]), + self.apertures[current_aperture], + self.steps_per_circle + ) + if not flash.is_empty: + if self.app.defaults['gerber_simplification']: + poly_buffer.append(flash.simplify(s_tol)) + else: + poly_buffer.append(flash) + + if self.is_lpc is True: + geo_dict['clear'] = flash + else: + geo_dict['solid'] = flash + + 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)) + + # 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 + + # ## G74/75* - Single or multiple quadrant arcs + match = self.quad_re.search(gline) + if match: + if match.group(1) == '4': + quadrant_mode = 'SINGLE' + else: + quadrant_mode = 'MULTI' + 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"] + + mode, circular_x, circular_y, i, j, d = match.groups() + + try: + circular_x = parse_gerber_number(circular_x, + self.int_digits, self.frac_digits, self.gerber_zeros) + except: + circular_x = current_x + + try: + circular_y = parse_gerber_number(circular_y, + self.int_digits, self.frac_digits, self.gerber_zeros) + except: + 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_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros) + except: + i = 0 + + try: + j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros) + except: + 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 + + 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 + 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 = sqrt(i ** 2 + j ** 2) + start = 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 = 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 = sqrt(i ** 2 + j ** 2) + + # Make sure radius to start is the same as radius to end. + radius2 = 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 = arctan2(-j, -i) # Start angle + stop = 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 / pi, stop * 180 / pi, arcdir[current_interpolation_mode], + angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2)) + + if angle <= (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) + + # ## EOF + match = self.eof_re.search(gline) + if match: + continue + + # ## Line did not match any pattern. Warn user. + log.warning("Line ignored (%d): %s" % (line_num, gline)) + + if len(path) > 1: + # In case that G01 (moving) aperture is rectangular, there is no need to still create + # another geo since we already created a shapely box using the start and end coordinates found in + # path variable. We do it only for other apertures than 'R' type + if self.apertures[last_path_aperture]["type"] == 'R': + pass + else: + # EOF, create shapely LineString if something still in path + # ## --- Buffered --- + + geo_dict = dict() + # 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 + width = self.apertures[last_path_aperture]["size"] + geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + 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 + + 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)) + + # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way + # find out the conversion factor used to convert inside the self.apertures keys: size, width, height + file_units = self.gerber_units if self.gerber_units else 'IN' + app_units = self.app.defaults['units'] + + conversion_factor = 25.4 if file_units == 'IN' else (1 / 25.4) if file_units != app_units else 1 + + # --- 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: + log.error("Object is not Gerber 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))) + + if self.use_buffer_for_union: + log.debug("Union by buffer...") + + 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 + + # try: + # self.solid_geometry = self.solid_geometry.union(new_poly) + # except Exception as e: + # # in case in the new_poly are some self intersections try to avoid making union with them + # for poly in new_poly: + # try: + # self.solid_geometry = self.solid_geometry.union(poly) + # except: + # pass + else: + self.solid_geometry = self.solid_geometry.difference(new_poly) + 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)) + + 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)) + + @staticmethod + def create_flash_geometry(location, aperture, steps_per_circle=None): + + # log.debug('Flashing @%s, Aperture: %s' % (location, aperture)) + + if type(location) == list: + location = Point(location) + + if aperture['type'] == 'C': # Circles + return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4)) + + if aperture['type'] == 'R': # Rectangles + loc = location.coords[0] + width = aperture['width'] + height = aperture['height'] + minx = loc[0] - width / 2 + maxx = loc[0] + width / 2 + miny = loc[1] - height / 2 + maxy = loc[1] + height / 2 + return shply_box(minx, miny, maxx, maxy) + + if aperture['type'] == 'O': # Obround + loc = location.coords[0] + width = aperture['width'] + height = aperture['height'] + if width > height: + p1 = Point(loc[0] + 0.5 * (width - height), loc[1]) + p2 = Point(loc[0] - 0.5 * (width - height), loc[1]) + c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4)) + c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4)) + else: + p1 = Point(loc[0], loc[1] + 0.5 * (height - width)) + p2 = Point(loc[0], loc[1] - 0.5 * (height - width)) + c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4)) + c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4)) + return cascaded_union([c1, c2]).convex_hull + + if aperture['type'] == 'P': # Regular polygon + loc = location.coords[0] + diam = aperture['diam'] + n_vertices = aperture['nVertices'] + points = [] + for i in range(0, n_vertices): + x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices)) + y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices)) + points.append((x, y)) + ply = Polygon(points) + if 'rotation' in aperture: + ply = affinity.rotate(ply, aperture['rotation']) + return ply + + if aperture['type'] == 'AM': # Aperture Macro + loc = location.coords[0] + flash_geo = aperture['macro'].make_geometry(aperture['modifiers']) + if flash_geo.is_empty: + log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name)) + return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1]) + + log.warning("Unknown aperture type: %s" % aperture['type']) + return None + + def create_geometry(self): + """ + Geometry from a Gerber file is made up entirely of polygons. + Every stroke (linear or circular) has an aperture which gives + it thickness. Additionally, aperture strokes have non-zero area, + and regions naturally do as well. + + :rtype : None + :return: None + """ + pass + # self.buffer_paths() + # + # self.fix_regions() + # + # self.do_flashes() + # + # self.solid_geometry = cascaded_union(self.buffered_paths + + # [poly['polygon'] for poly in self.regions] + + # self.flash_geometry) + + def get_bounding_box(self, margin=0.0, rounded=False): + """ + Creates and returns a rectangular polygon bounding at a distance of + margin from the object's ``solid_geometry``. If margin > 0, the polygon + can optionally have rounded corners of radius equal to margin. + + :param margin: Distance to enlarge the rectangular bounding + box in both positive and negative, x and y axes. + :type margin: float + :param rounded: Wether or not to have rounded corners. + :type rounded: bool + :return: The bounding box. + :rtype: Shapely.Polygon + """ + + bbox = self.solid_geometry.envelope.buffer(margin) + if not rounded: + bbox = bbox.envelope + return bbox + + def bounds(self): + """ + Returns coordinates of rectangular bounds + of Gerber geometry: (xmin, ymin, xmax, ymax). + """ + # fixed issue of getting bounds only for one level lists of objects + # now it can get bounds for nested lists of objects + + log.debug("camlib.Gerber.bounds()") + + if self.solid_geometry is None: + log.debug("solid_geometry is None") + return 0, 0, 0, 0 + + def bounds_rec(obj): + if type(obj) is list and type(obj) is not MultiPolygon: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in obj: + if type(k) is dict: + for key in k: + minx_, miny_, maxx_, maxy_ = bounds_rec(k[key]) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + else: + if not k.is_empty: + try: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + except Exception as e: + log.debug("camlib.Gerber.bounds() --> %s" % str(e)) + return + + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + return obj.bounds + + bounds_coords = bounds_rec(self.solid_geometry) + return bounds_coords + + def scale(self, xfactor, yfactor=None, point=None): + """ + Scales the objects' geometry on the XY plane by a given factor. + These are: + + * ``buffered_paths`` + * ``flash_geometry`` + * ``solid_geometry`` + * ``regions`` + + NOTE: + Does not modify the data used to create these elements. If these + are recreated, the scaling will be lost. This behavior was modified + because of the complexity reached in this class. + + :param xfactor: Number by which to scale on X axis. + :type xfactor: float + :param yfactor: Number by which to scale on Y axis. + :type yfactor: float + :rtype : None + """ + log.debug("camlib.Gerber.scale()") + + try: + xfactor = float(xfactor) + except: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Scale factor has to be a number: integer or float.")) + return + + if yfactor is None: + yfactor = xfactor + else: + try: + yfactor = float(yfactor) + except: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("Scale factor has to be a number: integer or float.")) + return + + if point is None: + px = 0 + py = 0 + else: + px, py = point + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def scale_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(scale_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return affinity.scale(obj, xfactor, yfactor, origin=(px, py)) + except AttributeError: + return obj + + self.solid_geometry = scale_geom(self.solid_geometry) + self.follow_geometry = scale_geom(self.follow_geometry) + + # we need to scale the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = scale_geom(geo_el['solid']) + if 'follow' in geo_el: + geo_el['follow'] = scale_geom(geo_el['follow']) + if 'clear' in geo_el: + geo_el['clear'] = scale_geom(geo_el['clear']) + + except Exception as e: + log.debug('camlib.Gerber.scale() Exception --> %s' % str(e)) + return 'fail' + + self.app.inform.emit('[success] %s' % + _("Gerber Scale done.")) + self.app.proc_container.new_text = '' + + # ## solid_geometry ??? + # It's a cascaded union of objects. + # self.solid_geometry = affinity.scale(self.solid_geometry, factor, + # factor, origin=(0, 0)) + + # # Now buffered_paths, flash_geometry and solid_geometry + # self.create_geometry() + + def offset(self, vect): + """ + Offsets the objects' geometry on the XY plane by a given vector. + These are: + + * ``buffered_paths`` + * ``flash_geometry`` + * ``solid_geometry`` + * ``regions`` + + NOTE: + Does not modify the data used to create these elements. If these + are recreated, the scaling will be lost. This behavior was modified + because of the complexity reached in this class. + + :param vect: (x, y) offset vector. + :type vect: tuple + :return: None + """ + log.debug("camlib.Gerber.offset()") + + try: + dx, dy = vect + except TypeError: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("An (x,y) pair of values are needed. " + "Probable you entered only one value in the Offset field.")) + return + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def offset_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(offset_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return affinity.translate(obj, xoff=dx, yoff=dy) + except AttributeError: + return obj + + # ## Solid geometry + self.solid_geometry = offset_geom(self.solid_geometry) + self.follow_geometry = offset_geom(self.follow_geometry) + + # we need to offset the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = offset_geom(geo_el['solid']) + if 'follow' in geo_el: + geo_el['follow'] = offset_geom(geo_el['follow']) + if 'clear' in geo_el: + geo_el['clear'] = offset_geom(geo_el['clear']) + + except Exception as e: + log.debug('camlib.Gerber.offset() Exception --> %s' % str(e)) + return 'fail' + + self.app.inform.emit('[success] %s' % + _("Gerber Offset done.")) + self.app.proc_container.new_text = '' + + def mirror(self, axis, point): + """ + Mirrors the object around a specified axis passing through + the given point. What is affected: + + * ``buffered_paths`` + * ``flash_geometry`` + * ``solid_geometry`` + * ``regions`` + + NOTE: + Does not modify the data used to create these elements. If these + are recreated, the scaling will be lost. This behavior was modified + because of the complexity reached in this class. + + :param axis: "X" or "Y" indicates around which axis to mirror. + :type axis: str + :param point: [x, y] point belonging to the mirror axis. + :type point: list + :return: None + """ + log.debug("camlib.Gerber.mirror()") + + px, py = point + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def mirror_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(mirror_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return affinity.scale(obj, xscale, yscale, origin=(px, py)) + except AttributeError: + return obj + + self.solid_geometry = mirror_geom(self.solid_geometry) + self.follow_geometry = mirror_geom(self.follow_geometry) + + # we need to mirror the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = mirror_geom(geo_el['solid']) + if 'follow' in geo_el: + geo_el['follow'] = mirror_geom(geo_el['follow']) + if 'clear' in geo_el: + geo_el['clear'] = mirror_geom(geo_el['clear']) + except Exception as e: + log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e)) + return 'fail' + + self.app.inform.emit('[success] %s' % + _("Gerber Mirror done.")) + self.app.proc_container.new_text = '' + + def skew(self, angle_x, angle_y, point): + """ + Shear/Skew the geometries of an object by angles along x and y dimensions. + + Parameters + ---------- + angle_x, angle_y : float, float + The shear angle(s) for the x and y axes respectively. These can be + specified in either degrees (default) or radians by setting + use_radians=True. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + log.debug("camlib.Gerber.skew()") + + px, py = point + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def skew_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(skew_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return affinity.skew(obj, angle_x, angle_y, origin=(px, py)) + except AttributeError: + return obj + + self.solid_geometry = skew_geom(self.solid_geometry) + self.follow_geometry = skew_geom(self.follow_geometry) + + # we need to skew the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = skew_geom(geo_el['solid']) + if 'follow' in geo_el: + geo_el['follow'] = skew_geom(geo_el['follow']) + if 'clear' in geo_el: + geo_el['clear'] = skew_geom(geo_el['clear']) + except Exception as e: + log.debug('camlib.Gerber.skew() Exception --> %s' % str(e)) + return 'fail' + + self.app.inform.emit('[success] %s' % + _("Gerber Skew done.")) + self.app.proc_container.new_text = '' + + def rotate(self, angle, point): + """ + Rotate an object by a given angle around given coords (point) + :param angle: + :param point: + :return: + """ + log.debug("camlib.Gerber.rotate()") + + px, py = point + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for g in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def rotate_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(rotate_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return affinity.rotate(obj, angle, origin=(px, py)) + except AttributeError: + return obj + + self.solid_geometry = rotate_geom(self.solid_geometry) + self.follow_geometry = rotate_geom(self.follow_geometry) + + # we need to rotate the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = rotate_geom(geo_el['solid']) + if 'follow' in geo_el: + geo_el['follow'] = rotate_geom(geo_el['follow']) + if 'clear' in geo_el: + geo_el['clear'] = rotate_geom(geo_el['clear']) + except Exception as e: + log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e)) + return 'fail' + self.app.inform.emit('[success] %s' % + _("Gerber Rotate done.")) + self.app.proc_container.new_text = ''