|
|
@@ -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)
|