2019-12-06 22:25:22 +00:00
|
|
|
# ########################################################## ##
|
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
|
# http://flatcam.org #
|
|
|
|
# Author: Juan Pablo Caram (c) #
|
|
|
|
# Date: 2/5/2014 #
|
|
|
|
# MIT Licence #
|
|
|
|
# ########################################################## ##
|
2019-10-15 23:28:18 +00:00
|
|
|
|
2020-06-02 15:29:45 +00:00
|
|
|
from camlib import Geometry, grace
|
2019-10-15 23:28:18 +00:00
|
|
|
|
|
|
|
import shapely.affinity as affinity
|
|
|
|
from shapely.geometry import Point, LineString
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
import re
|
|
|
|
import logging
|
|
|
|
import traceback
|
2019-12-05 23:20:33 +00:00
|
|
|
from copy import deepcopy
|
2019-10-06 12:56:41 +00:00
|
|
|
|
2020-06-02 15:29:45 +00:00
|
|
|
# import AppTranslation as fcTranslate
|
2019-10-06 12:56:41 +00:00
|
|
|
|
|
|
|
import gettext
|
|
|
|
import builtins
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
if '_' not in builtins.__dict__:
|
|
|
|
_ = gettext.gettext
|
|
|
|
|
2019-10-15 23:28:18 +00:00
|
|
|
log = logging.getLogger('base')
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
================ ====================================
|
2020-06-16 00:47:26 +00:00
|
|
|
tooldia Diameter of the tool
|
|
|
|
drills List that store the Shapely Points for drill points
|
2020-07-05 21:20:20 +00:00
|
|
|
slots List that store the Shapely Points for slots. Each is a tuple: (start_point, stop_point)
|
2020-02-17 02:43:01 +00:00
|
|
|
data dictionary which holds the options for each tool
|
2020-06-16 00:47:26 +00:00
|
|
|
solid_geometry Geometry list for each tool
|
2019-10-06 04:14:10 +00:00
|
|
|
================ ====================================
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
2020-04-27 07:03:22 +00:00
|
|
|
|
2019-12-05 13:18:54 +00:00
|
|
|
self.decimals = self.app.decimals
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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
|
2020-02-29 22:52:24 +00:00
|
|
|
self.tools = {}
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
# ## IN|MM -> Units are inherited from Geometry
|
2019-12-06 15:12:18 +00:00
|
|
|
self.units = self.app.defaults['units']
|
2020-06-07 14:06:23 +00:00
|
|
|
self.units_found = self.app.defaults['units']
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# Trailing "T" or leading "L" (default)
|
|
|
|
# self.zeros = "T"
|
|
|
|
self.zeros = zeros or self.defaults["zeros"]
|
2019-12-10 21:57:00 +00:00
|
|
|
self.zeros_found = deepcopy(self.zeros)
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# 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"]
|
2020-06-07 14:06:23 +00:00
|
|
|
self.excellon_units_found = None
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
# 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.
|
2020-07-14 13:46:01 +00:00
|
|
|
self.ser_attrs += ['zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
|
2020-06-16 00:47:26 +00:00
|
|
|
'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'source_file']
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# ### 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):
|
|
|
|
"""
|
2020-04-27 07:03:22 +00:00
|
|
|
Reads the specified file as array of lines as passes it to ``parse_lines()``.
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
:param filename: The file to be read and parsed.
|
|
|
|
:param file_obj:
|
|
|
|
:type filename: str
|
|
|
|
:return: None
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
|
|
|
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)
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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 = ''
|
|
|
|
|
2019-10-06 12:56:41 +00:00
|
|
|
# ## Parsing starts here ## ##
|
2019-10-06 04:14:10 +00:00
|
|
|
line_num = 0 # Line number
|
|
|
|
eline = ""
|
|
|
|
try:
|
|
|
|
for eline in elines:
|
|
|
|
if self.app.abort_flag:
|
|
|
|
# graceful abort requested by the user
|
2020-04-27 07:03:22 +00:00
|
|
|
raise grace
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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)
|
2019-12-06 15:12:18 +00:00
|
|
|
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_('This is GCODE mark'), eline))
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a TOOL ------------ #
|
|
|
|
if name_tool not in self.tools:
|
|
|
|
self.tools[name_tool] = {}
|
2019-10-06 04:14:10 +00:00
|
|
|
if line_units == 'MILS':
|
2020-06-16 00:47:26 +00:00
|
|
|
spec = {
|
|
|
|
'tooldia': (float(match.group(2)) / 1000)
|
|
|
|
}
|
|
|
|
self.tools[name_tool]['tooldia'] = (float(match.group(2)) / 1000)
|
|
|
|
log.debug("Tool definition: %d %s" % (name_tool, spec))
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
2020-06-16 00:47:26 +00:00
|
|
|
spec = {
|
|
|
|
'tooldia': float(match.group(2))
|
|
|
|
}
|
|
|
|
self.tools[name_tool]['tooldia'] = float(match.group(2))
|
|
|
|
log.debug("Tool definition: %d %s" % (name_tool, spec))
|
2019-10-06 04:14:10 +00:00
|
|
|
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)
|
2020-06-07 14:06:23 +00:00
|
|
|
log.warning("Excellon format preset found in comments: %s:%s" %
|
2019-10-06 04:14:10 +00:00
|
|
|
(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)
|
2020-06-07 14:06:23 +00:00
|
|
|
|
|
|
|
'''
|
|
|
|
In case that the units were not found in the header, we have two choices:
|
|
|
|
- one is to use the default value in the App Preferences
|
|
|
|
- the other is to make an evaluation based on a threshold
|
|
|
|
we process here the self.tools list and make a list with tools with diameter less or equal
|
|
|
|
with 0.1 and a list with tools with value greater than 0.1, 0.1 being the threshold value.
|
|
|
|
Most tools in Excellon are greater than 0.1mm therefore if most of the tools are under this
|
|
|
|
value it is safe to assume that the units are in INCH
|
|
|
|
'''
|
|
|
|
greater_tools = set()
|
|
|
|
lower_tools = set()
|
|
|
|
if not self.excellon_units_found and self.tools:
|
|
|
|
for tool in self.tools:
|
2020-06-16 00:47:26 +00:00
|
|
|
tool_dia = float(self.tools[tool]['tooldia'])
|
2020-06-07 14:06:23 +00:00
|
|
|
lower_tools.add(tool_dia) if tool_dia <= 0.1 else greater_tools.add(tool_dia)
|
|
|
|
|
|
|
|
assumed_units = "IN" if len(lower_tools) > len(greater_tools) else "MM"
|
|
|
|
self.units = assumed_units
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2019-12-06 15:12:18 +00:00
|
|
|
self.units = {"1": "MM", "2": "IN"}[match.group(1)]
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# Modified for issue #80
|
2019-12-06 15:12:18 +00:00
|
|
|
log.debug("ALternative M71/M72 units found, before conversion: %s" % self.units)
|
|
|
|
self.convert_units(self.units)
|
|
|
|
log.debug("ALternative M71/M72 units found, after conversion: %s" % self.units)
|
2019-10-06 04:14:10 +00:00
|
|
|
if self.units == 'MM':
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# ### Body ####
|
|
|
|
if not in_header:
|
|
|
|
|
|
|
|
# ## Tool change ###
|
|
|
|
match = self.toolsel_re.search(eline)
|
|
|
|
if match:
|
2020-06-16 00:47:26 +00:00
|
|
|
current_tool = int(match.group(1))
|
2019-10-06 04:14:10 +00:00
|
|
|
log.debug("Tool change: %s" % current_tool)
|
|
|
|
if bool(headerless):
|
|
|
|
match = self.toolset_hl_re.search(eline)
|
|
|
|
if match:
|
2020-06-16 00:47:26 +00:00
|
|
|
name = int(match.group(1))
|
2019-10-06 04:14:10 +00:00
|
|
|
try:
|
|
|
|
diam = float(match.group(2))
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
# 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
|
2020-05-02 12:56:30 +00:00
|
|
|
# the below construction is so each tool will have a slightly different diameter
|
2019-10-06 04:14:10 +00:00
|
|
|
# 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
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# ---------- add a TOOL ------------ #
|
|
|
|
spec = {"tooldia": diam, 'solid_geometry': []}
|
|
|
|
if name not in self.tools:
|
|
|
|
self.tools[name] = {}
|
|
|
|
self.tools[name]['tooldia'] = diam
|
|
|
|
self.tools[name]['solid_geometry'] = []
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-06-16 00:47:26 +00:00
|
|
|
current_tool = name_tool
|
|
|
|
log.debug("Tool change for Allegro type of Excellon: %d" % current_tool)
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
slot_dia = float(self.tools[current_tool]['tooldia'])
|
2020-04-27 07:03:22 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
pass
|
|
|
|
log.debug(
|
|
|
|
'Milling/Drilling slot with tool %s, diam=%f' % (
|
|
|
|
current_tool,
|
|
|
|
slot_dia
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# ---------- add a slot ------------ #
|
|
|
|
slot = (
|
|
|
|
Point(slot_start_x, slot_start_y),
|
|
|
|
Point(slot_stop_x, slot_stop_y)
|
2019-10-06 04:14:10 +00:00
|
|
|
)
|
2020-06-16 00:47:26 +00:00
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'slots' in self.tools[current_tool]:
|
2020-06-16 09:27:48 +00:00
|
|
|
self.tools[current_tool]['slots'].append(slot)
|
2020-06-16 00:47:26 +00:00
|
|
|
else:
|
|
|
|
self.tools[current_tool]['slots'] = [slot]
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-11-11 00:35:42 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
slot_dia = float(self.tools[current_tool]['tooldia'])
|
2020-04-27 07:03:22 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
pass
|
|
|
|
log.debug(
|
|
|
|
'Milling/Drilling slot with tool %s, diam=%f' % (
|
|
|
|
current_tool,
|
|
|
|
slot_dia
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# ---------- add a Slot ------------ #
|
|
|
|
slot = (
|
|
|
|
Point(slot_start_x, slot_start_y),
|
|
|
|
Point(slot_stop_x, slot_stop_y)
|
2019-10-06 04:14:10 +00:00
|
|
|
)
|
2020-06-16 00:47:26 +00:00
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'slots' in self.tools[current_tool]:
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[current_tool]['slots'].append(slot)
|
2020-06-16 00:47:26 +00:00
|
|
|
else:
|
|
|
|
self.tools[current_tool]['slots'] = [slot]
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# ## Coordinates without period # ##
|
|
|
|
match = self.coordsnoperiod_re.search(eline)
|
|
|
|
if match:
|
|
|
|
matchr = self.repeat_re.search(eline)
|
2020-04-04 15:02:40 +00:00
|
|
|
if matchr: # if we have a repeat command
|
2019-10-06 04:14:10 +00:00
|
|
|
repeat = int(matchr.group(1))
|
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
if match.group(1):
|
|
|
|
repeating_x = self.parse_number(match.group(1))
|
|
|
|
else:
|
|
|
|
repeating_x = 0
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
if match.group(2):
|
|
|
|
repeating_y = self.parse_number(match.group(2))
|
|
|
|
else:
|
|
|
|
repeating_y = 0
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
coordx = current_x
|
|
|
|
coordy = current_y
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
while repeat > 0:
|
|
|
|
if repeating_x:
|
|
|
|
coordx += repeating_x
|
|
|
|
if repeating_y:
|
|
|
|
coordy += repeating_y
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Drill ------------ #
|
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'drills' in self.tools[current_tool]:
|
|
|
|
self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
|
|
|
|
else:
|
|
|
|
self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
|
2020-04-04 15:02:40 +00:00
|
|
|
|
|
|
|
repeat -= 1
|
|
|
|
current_x = coordx
|
|
|
|
current_y = coordy
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
else: # those are normal coordinates
|
|
|
|
try:
|
|
|
|
x = self.parse_number(match.group(1))
|
|
|
|
current_x = x
|
|
|
|
except TypeError:
|
|
|
|
x = current_x
|
|
|
|
except Exception:
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
y = self.parse_number(match.group(2))
|
|
|
|
current_y = y
|
|
|
|
except TypeError:
|
|
|
|
y = current_y
|
|
|
|
except Exception:
|
|
|
|
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'
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# signal that there are milling slots operations
|
|
|
|
self.defaults['excellon_drills'] = False
|
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
self.routing_flag = 0
|
|
|
|
slot_start_x = x
|
|
|
|
slot_start_y = y
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
2020-04-04 15:02:40 +00:00
|
|
|
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
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Slot ------------ #
|
|
|
|
slot = (
|
|
|
|
Point(slot_start_x, slot_start_y),
|
|
|
|
Point(slot_stop_x, slot_stop_y)
|
2020-04-04 15:02:40 +00:00
|
|
|
)
|
2020-06-16 00:47:26 +00:00
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'slots' in self.tools[current_tool]:
|
2020-06-16 09:27:48 +00:00
|
|
|
self.tools[current_tool]['slots'].append(slot)
|
2020-06-16 00:47:26 +00:00
|
|
|
else:
|
|
|
|
self.tools[current_tool]['slots'] = [slot]
|
2020-04-04 15:02:40 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
if self.match_routing_start is None and self.match_routing_stop is None:
|
2019-10-06 04:14:10 +00:00
|
|
|
# signal that there are drill operations
|
|
|
|
self.defaults['excellon_drills'] = True
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Drill ------------ #
|
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'drills' in self.tools[current_tool]:
|
|
|
|
self.tools[current_tool]['drills'].append(Point((x, y)))
|
|
|
|
else:
|
|
|
|
self.tools[current_tool]['drills'] = [Point((x, y))]
|
2020-04-04 15:02:40 +00:00
|
|
|
# log.debug("{:15} {:8} {:8}".format(eline, x, y))
|
|
|
|
continue
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# ## 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
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Slot ------------ #
|
|
|
|
slot = (
|
|
|
|
Point(slot_start_x, slot_start_y),
|
|
|
|
Point(slot_stop_x, slot_stop_y)
|
2019-10-06 04:14:10 +00:00
|
|
|
)
|
2020-06-16 00:47:26 +00:00
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'slots' in self.tools[current_tool]:
|
2020-06-16 09:27:48 +00:00
|
|
|
self.tools[current_tool]['slots'].append(slot)
|
2020-06-16 00:47:26 +00:00
|
|
|
else:
|
|
|
|
self.tools[current_tool]['slots'] = [slot]
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Drill ------------ #
|
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'drills' in self.tools[current_tool]:
|
|
|
|
self.tools[current_tool]['drills'].append(Point((x, y)))
|
|
|
|
else:
|
|
|
|
self.tools[current_tool]['drills'] = [Point((x, y))]
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
|
|
|
coordx = x
|
|
|
|
coordy = y
|
|
|
|
while repeat > 0:
|
|
|
|
if repeating_x:
|
|
|
|
coordx = (repeat * x) + repeating_x
|
|
|
|
if repeating_y:
|
|
|
|
coordy = (repeat * y) + repeating_y
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# ---------- add a Drill ------------ #
|
|
|
|
if current_tool not in self.tools:
|
|
|
|
self.tools[current_tool] = {}
|
|
|
|
if 'drills' in self.tools[current_tool]:
|
|
|
|
self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
|
|
|
|
else:
|
|
|
|
self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
# ---------- add a TOOL ------------ #
|
|
|
|
name = int(match.group(1))
|
2019-10-06 04:14:10 +00:00
|
|
|
spec = {"C": float(match.group(2)), 'solid_geometry': []}
|
2020-06-16 00:47:26 +00:00
|
|
|
if name not in self.tools:
|
|
|
|
self.tools[name] = {}
|
|
|
|
self.tools[name]['tooldia'] = float(match.group(2))
|
|
|
|
self.tools[name]['solid_geometry'] = []
|
|
|
|
|
2019-12-06 19:39:06 +00:00
|
|
|
log.debug("Tool definition: %s %s" % (name, spec))
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# ## Units and number format # ##
|
|
|
|
match = self.units_re.match(eline)
|
|
|
|
if match:
|
2019-12-10 15:42:51 +00:00
|
|
|
self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
|
2020-06-07 14:06:23 +00:00
|
|
|
self.excellon_units_found = self.units
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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])
|
2019-12-06 15:12:18 +00:00
|
|
|
if self.units == 'MM':
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-06-07 14:06:23 +00:00
|
|
|
log.warning("UNITS found inline - Value before conversion: %s" % self.units)
|
2019-12-06 15:12:18 +00:00
|
|
|
self.convert_units(self.units)
|
2020-06-07 14:06:23 +00:00
|
|
|
log.warning("UNITS found inline - Value after conversion: %s" % self.units)
|
2019-10-06 04:14:10 +00:00
|
|
|
if self.units == 'MM':
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
|
|
|
|
log.warning("Type of ZEROS found inline, in header: %s" % self.zeros)
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Search for units type again it might be alone on the line
|
|
|
|
if "INCH" in eline:
|
2019-12-06 15:12:18 +00:00
|
|
|
line_units = "IN"
|
2019-10-06 04:14:10 +00:00
|
|
|
# Modified for issue #80
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Type of UNITS found inline, in header, before conversion: %s" % line_units)
|
|
|
|
self.convert_units(line_units)
|
|
|
|
log.warning("Type of UNITS found inline, in header, after conversion: %s" % self.units)
|
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
|
2020-06-07 14:06:23 +00:00
|
|
|
self.excellon_units_found = "IN"
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
elif "METRIC" in eline:
|
2019-12-06 15:12:18 +00:00
|
|
|
line_units = "MM"
|
2019-10-06 04:14:10 +00:00
|
|
|
# Modified for issue #80
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Type of UNITS found inline, in header, before conversion: %s" % line_units)
|
|
|
|
self.convert_units(line_units)
|
|
|
|
log.warning("Type of UNITS found inline, in header, after conversion: %s" % self.units)
|
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
|
2020-06-07 14:06:23 +00:00
|
|
|
self.excellon_units_found = "MM"
|
2019-10-06 04:14:10 +00:00
|
|
|
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()
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Type of ZEROS found: %s" % self.zeros)
|
2019-10-06 04:14:10 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# ## Units and number format outside header# ##
|
|
|
|
match = self.units_re.match(eline)
|
|
|
|
if match:
|
2020-06-07 14:06:23 +00:00
|
|
|
self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
|
|
|
|
self.excellon_units_found = self.units
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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])
|
2019-12-06 15:12:18 +00:00
|
|
|
if self.units == 'MM':
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Type of UNITS found outside header, inline before conversion: %s" % self.units)
|
|
|
|
self.convert_units(self.units)
|
|
|
|
log.warning("Type of UNITS found outside header, inline after conversion: %s" % self.units)
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
if self.units == 'MM':
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%s" %
|
|
|
|
(str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
2019-12-06 15:12:18 +00:00
|
|
|
log.warning("Excellon format preset is: %s:%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)
|
2019-10-06 04:14:10 +00:00
|
|
|
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']
|
2019-12-06 15:12:18 +00:00
|
|
|
|
2020-06-18 17:24:02 +00:00
|
|
|
# the data structure of the Excellon object has to include bot the 'drills' and the 'slots' keys otherwise
|
|
|
|
# I will need to test for them everywhere.
|
|
|
|
# Even if there are not drills or slots I just add the storage there with an empty list
|
|
|
|
for tool in self.tools:
|
|
|
|
if 'drills' not in self.tools[tool]:
|
|
|
|
self.tools[tool]['drills'] = []
|
|
|
|
if 'slots' not in self.tools[tool]:
|
|
|
|
self.tools[tool]['slots'] = []
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
|
2019-12-05 23:20:33 +00:00
|
|
|
except Exception:
|
2019-10-06 04:14:10 +00:00
|
|
|
log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
|
2020-04-27 07:03:22 +00:00
|
|
|
msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
|
|
|
|
msg += '{e_code} {tx} {l_nr}: {line}\n'.format(
|
2019-10-06 04:14:10 +00:00
|
|
|
e_code='[ERROR]',
|
2019-12-13 16:36:57 +00:00
|
|
|
tx=_("Excellon Parser error.\nParsing Failed. Line"),
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-06-16 00:47:26 +00:00
|
|
|
specified in self.tools[tool]['drills'].
|
|
|
|
Also creates geometries (polygons)
|
|
|
|
for the slots as specified in self.tools[tool]['slots']
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
|
|
|
"""
|
2019-12-06 15:12:18 +00:00
|
|
|
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.create_geometry()")
|
2019-10-06 04:14:10 +00:00
|
|
|
self.solid_geometry = []
|
|
|
|
try:
|
|
|
|
# clear the solid_geometry in self.tools
|
|
|
|
for tool in self.tools:
|
2020-02-29 22:52:24 +00:00
|
|
|
self.tools[tool]['solid_geometry'] = []
|
|
|
|
self.tools[tool]['data'] = {}
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
tooldia = self.tools[tool]['tooldia']
|
|
|
|
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
poly = drill.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
|
|
|
|
|
|
|
|
# add poly in the tools geometry
|
|
|
|
self.tools[tool]['solid_geometry'].append(poly)
|
|
|
|
self.tools[tool]['data'] = deepcopy(self.default_data)
|
|
|
|
|
|
|
|
# add poly to the total solid geometry
|
|
|
|
self.solid_geometry.append(poly)
|
|
|
|
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
start = slot[0]
|
|
|
|
stop = slot[1]
|
|
|
|
|
|
|
|
lines_string = LineString([start, stop])
|
|
|
|
poly = lines_string.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
|
|
|
|
|
|
|
|
# add poly in the tools geometry
|
|
|
|
self.tools[tool]['solid_geometry'].append(poly)
|
|
|
|
self.tools[tool]['data'] = deepcopy(self.default_data)
|
|
|
|
|
|
|
|
# add poly to the total solid geometry
|
|
|
|
self.solid_geometry.append(poly)
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
except Exception as e:
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.create_geometry() -> "
|
2019-12-06 19:39:06 +00:00
|
|
|
"Excellon geometry creation failed due of ERROR: %s" % str(e))
|
2019-10-06 04:14:10 +00:00
|
|
|
return "fail"
|
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
def bounds(self, flatten=None):
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
|
|
|
Returns coordinates of rectangular bounds
|
|
|
|
of Excellon geometry: (xmin, ymin, xmax, ymax).
|
2020-04-27 07:03:22 +00:00
|
|
|
|
|
|
|
:param flatten: No used
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
|
|
|
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.bounds()")
|
2019-12-06 19:53:03 +00:00
|
|
|
|
|
|
|
if self.solid_geometry is None or not self.tools:
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon -> solid_geometry is None")
|
2019-10-06 04:14:10 +00:00
|
|
|
return 0, 0, 0, 0
|
|
|
|
|
|
|
|
def bounds_rec(obj):
|
|
|
|
if type(obj) is list:
|
2019-10-15 23:28:18 +00:00
|
|
|
minx = np.Inf
|
|
|
|
miny = np.Inf
|
|
|
|
maxx = -np.Inf
|
|
|
|
maxy = -np.Inf
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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:
|
2020-04-27 07:03:22 +00:00
|
|
|
eminx, eminy, emaxx, emaxy = bounds_rec(self.tools[tool]['solid_geometry'])
|
|
|
|
minx_list.append(eminx)
|
|
|
|
miny_list.append(eminy)
|
|
|
|
maxx_list.append(emaxx)
|
|
|
|
maxy_list.append(emaxy)
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2019-12-06 19:53:03 +00:00
|
|
|
return min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2020-06-02 15:29:45 +00:00
|
|
|
On object creation, in app_obj.new_object(), true conversion is done because this is done at the end of the
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
2020-04-27 07:03:22 +00:00
|
|
|
|
|
|
|
:param units: 'IN' or 'MM'. String
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
|
2019-12-06 15:12:18 +00:00
|
|
|
# factor = Geometry.convert_units(self, units)
|
|
|
|
obj_units = units
|
|
|
|
if obj_units.upper() == self.units.upper():
|
|
|
|
factor = 1.0
|
|
|
|
elif obj_units.upper() == "MM":
|
|
|
|
factor = 25.4
|
|
|
|
elif obj_units.upper() == "IN":
|
|
|
|
factor = 1 / 25.4
|
|
|
|
else:
|
|
|
|
log.error("Unsupported units: %s" % str(obj_units))
|
|
|
|
factor = 1.0
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.convert_units() --> Factor: %s" % str(factor))
|
2019-12-06 15:12:18 +00:00
|
|
|
|
|
|
|
self.units = obj_units
|
|
|
|
self.scale(factor, factor)
|
|
|
|
self.file_units_factor = factor
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
# Tools
|
|
|
|
for tname in self.tools:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.tools[tname]["tooldia"] *= factor
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
:param xfactor: Number by which to scale the object.
|
|
|
|
:type xfactor: float
|
|
|
|
:param yfactor: Number by which to scale the object.
|
|
|
|
:type yfactor: float
|
|
|
|
:param point: Origin point for scale
|
|
|
|
:return: None
|
|
|
|
:rtype: None
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.scale()")
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
if yfactor is None:
|
|
|
|
yfactor = xfactor
|
|
|
|
|
|
|
|
if point is None:
|
|
|
|
px = 0
|
|
|
|
py = 0
|
|
|
|
else:
|
|
|
|
px, py = point
|
|
|
|
|
2019-12-09 14:20:22 +00:00
|
|
|
if xfactor == 0 and yfactor == 0:
|
|
|
|
return
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.geo_len = len(self.tools)
|
2019-10-06 04:14:10 +00:00
|
|
|
except TypeError:
|
|
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
|
|
self.el_count = 0
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
# Scale Drills
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
new_drills = []
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
new_drills.append(affinity.scale(drill, xfactor, yfactor, origin=(px, py)))
|
|
|
|
self.tools[tool]['drills'] = new_drills
|
|
|
|
|
|
|
|
# Scale Slots
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
new_slots = []
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
new_start = affinity.scale(slot[0], xfactor, yfactor, origin=(px, py))
|
|
|
|
new_stop = affinity.scale(slot[1], xfactor, yfactor, origin=(px, py))
|
|
|
|
new_slot = (new_start, new_stop)
|
|
|
|
new_slots.append(new_slot)
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[tool]['slots'] = new_slots
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# Scale solid_geometry
|
|
|
|
self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# update status display
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.offset()")
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
dx, dy = vect
|
|
|
|
|
2019-12-09 14:20:22 +00:00
|
|
|
if dx == 0 and dy == 0:
|
|
|
|
return
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
def offset_geom(obj):
|
2020-04-27 07:03:22 +00:00
|
|
|
try:
|
2019-10-06 04:14:10 +00:00
|
|
|
new_obj = []
|
2020-04-27 07:03:22 +00:00
|
|
|
for geo in obj:
|
|
|
|
new_obj.append(offset_geom(geo))
|
2019-10-06 04:14:10 +00:00
|
|
|
return new_obj
|
2020-04-27 07:03:22 +00:00
|
|
|
except TypeError:
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.geo_len = len(self.tools)
|
2019-10-06 04:14:10 +00:00
|
|
|
except TypeError:
|
|
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
|
|
self.el_count = 0
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
# Offset Drills
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
new_drills = []
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
new_drills.append(affinity.translate(drill, xoff=dx, yoff=dy))
|
|
|
|
self.tools[tool]['drills'] = new_drills
|
|
|
|
|
|
|
|
# Offset Slots
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
new_slots = []
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
new_start = affinity.translate(slot[0], xoff=dx, yoff=dy)
|
|
|
|
new_stop = affinity.translate(slot[1], xoff=dx, yoff=dy)
|
|
|
|
new_slot = (new_start, new_stop)
|
|
|
|
new_slots.append(new_slot)
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[tool]['slots'] = new_slots
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# Offset solid_geometry
|
|
|
|
self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# update status display
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
# Recreate geometry
|
|
|
|
self.create_geometry()
|
|
|
|
self.app.proc_container.new_text = ''
|
|
|
|
|
|
|
|
def mirror(self, axis, point):
|
|
|
|
"""
|
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
: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
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.mirror()")
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
px, py = point
|
|
|
|
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
|
|
|
|
|
|
|
|
def mirror_geom(obj):
|
2020-04-27 07:03:22 +00:00
|
|
|
try:
|
2019-10-06 04:14:10 +00:00
|
|
|
new_obj = []
|
2020-04-27 07:03:22 +00:00
|
|
|
for geo in obj:
|
|
|
|
new_obj.append(mirror_geom(geo))
|
2019-10-06 04:14:10 +00:00
|
|
|
return new_obj
|
2020-04-27 07:03:22 +00:00
|
|
|
except TypeError:
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.geo_len = len(self.tools)
|
2019-10-06 04:14:10 +00:00
|
|
|
except TypeError:
|
|
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
|
|
self.el_count = 0
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
# Offset Drills
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
new_drills = []
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
new_drills.append(affinity.scale(drill, xscale, yscale, origin=(px, py)))
|
|
|
|
self.tools[tool]['drills'] = new_drills
|
|
|
|
|
|
|
|
# Offset Slots
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
new_slots = []
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
new_start = affinity.scale(slot[0], xscale, yscale, origin=(px, py))
|
|
|
|
new_stop = affinity.scale(slot[1], xscale, yscale, origin=(px, py))
|
|
|
|
new_slot = (new_start, new_stop)
|
|
|
|
new_slots.append(new_slot)
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[tool]['slots'] = new_slots
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# Offset solid_geometry
|
|
|
|
self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
# update status display
|
2019-10-06 04:14:10 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
:param angle_x:
|
|
|
|
:param angle_y:
|
2019-10-06 04:14:10 +00:00
|
|
|
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.
|
2020-04-27 07:03:22 +00:00
|
|
|
:param point: Origin point for Skew
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
See shapely manual for more information:
|
|
|
|
http://toblerity.org/shapely/manual.html#affine-transformations
|
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.skew()")
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
if angle_x is None:
|
|
|
|
angle_x = 0.0
|
|
|
|
|
|
|
|
if angle_y is None:
|
|
|
|
angle_y = 0.0
|
|
|
|
|
2019-12-09 14:20:22 +00:00
|
|
|
if angle_x == 0 and angle_y == 0:
|
|
|
|
return
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
def skew_geom(obj):
|
2020-04-27 07:03:22 +00:00
|
|
|
try:
|
2019-10-06 04:14:10 +00:00
|
|
|
new_obj = []
|
|
|
|
for g in obj:
|
|
|
|
new_obj.append(skew_geom(g))
|
|
|
|
return new_obj
|
2020-04-27 07:03:22 +00:00
|
|
|
except TypeError:
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.geo_len = len(self.tools)
|
2019-10-06 04:14:10 +00:00
|
|
|
except TypeError:
|
|
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
|
|
self.el_count = 0
|
|
|
|
|
|
|
|
if point is None:
|
|
|
|
px, py = 0, 0
|
|
|
|
else:
|
|
|
|
px, py = point
|
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
# Offset Drills
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
new_drills = []
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
new_drills.append(affinity.skew(drill, angle_x, angle_y, origin=(px, py)))
|
|
|
|
self.tools[tool]['drills'] = new_drills
|
|
|
|
|
|
|
|
# Offset Slots
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
new_slots = []
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
new_start = affinity.skew(slot[0], angle_x, angle_y, origin=(px, py))
|
|
|
|
new_stop = affinity.skew(slot[1], angle_x, angle_y, origin=(px, py))
|
|
|
|
new_slot = (new_start, new_stop)
|
|
|
|
new_slots.append(new_slot)
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[tool]['slots'] = new_slots
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# Offset solid_geometry
|
|
|
|
self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
|
|
|
|
|
|
|
|
# update status display
|
|
|
|
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
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
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
|
2020-04-27 07:03:22 +00:00
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
:param angle:
|
2020-04-27 07:03:22 +00:00
|
|
|
:param point: tuple of coordinates (x, y)
|
|
|
|
:return: None
|
2019-10-06 04:14:10 +00:00
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.rotate()")
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2019-12-09 14:20:22 +00:00
|
|
|
if angle == 0:
|
|
|
|
return
|
|
|
|
|
2019-10-06 04:14:10 +00:00
|
|
|
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:
|
2020-06-16 00:47:26 +00:00
|
|
|
return affinity.rotate(obj, angle, origin=orig)
|
2019-10-06 04:14:10 +00:00
|
|
|
except AttributeError:
|
|
|
|
return obj
|
|
|
|
|
|
|
|
# variables to display the percentage of work done
|
|
|
|
self.geo_len = 0
|
|
|
|
try:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.geo_len = len(self.tools)
|
2019-10-06 04:14:10 +00:00
|
|
|
except TypeError:
|
|
|
|
self.geo_len = 1
|
|
|
|
self.old_disp_number = 0
|
|
|
|
self.el_count = 0
|
|
|
|
|
|
|
|
if point is None:
|
2020-06-16 00:47:26 +00:00
|
|
|
orig = 'center'
|
2019-10-06 04:14:10 +00:00
|
|
|
else:
|
2020-06-16 00:47:26 +00:00
|
|
|
orig = point
|
2019-10-06 04:14:10 +00:00
|
|
|
|
2020-06-16 00:47:26 +00:00
|
|
|
for tool in self.tools:
|
|
|
|
# Offset Drills
|
|
|
|
if 'drills' in self.tools[tool]:
|
|
|
|
new_drills = []
|
|
|
|
for drill in self.tools[tool]['drills']:
|
|
|
|
new_drills.append(affinity.rotate(drill, angle, origin=orig))
|
|
|
|
self.tools[tool]['drills'] = new_drills
|
|
|
|
|
|
|
|
# Offset Slots
|
|
|
|
if 'slots' in self.tools[tool]:
|
|
|
|
new_slots = []
|
|
|
|
for slot in self.tools[tool]['slots']:
|
|
|
|
new_start = affinity.rotate(slot[0], angle, origin=orig)
|
|
|
|
new_stop = affinity.rotate(slot[1], angle, origin=orig)
|
|
|
|
new_slot = (new_start, new_stop)
|
|
|
|
new_slots.append(new_slot)
|
2020-06-16 08:59:51 +00:00
|
|
|
self.tools[tool]['slots'] = new_slots
|
2020-06-16 00:47:26 +00:00
|
|
|
|
|
|
|
# Offset solid_geometry
|
|
|
|
self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin=orig)
|
|
|
|
|
|
|
|
# update status display
|
|
|
|
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
|
2019-10-06 04:14:10 +00:00
|
|
|
|
|
|
|
self.create_geometry()
|
2019-12-23 20:59:01 +00:00
|
|
|
self.app.proc_container.new_text = ''
|
|
|
|
|
2019-12-29 23:18:56 +00:00
|
|
|
def buffer(self, distance, join, factor):
|
2019-12-23 20:59:01 +00:00
|
|
|
"""
|
|
|
|
|
2020-04-27 07:03:22 +00:00
|
|
|
:param distance: if 'factor' is True then distance is the factor
|
|
|
|
:param factor: True or False (None)
|
|
|
|
:param join: The type of line joint used by the shapely buffer method: round, square, bevel
|
|
|
|
:return: None
|
2019-12-23 20:59:01 +00:00
|
|
|
"""
|
2020-06-03 17:35:59 +00:00
|
|
|
log.debug("appParsers.ParseExcellon.Excellon.buffer()")
|
2019-12-23 20:59:01 +00:00
|
|
|
|
|
|
|
if distance == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
def buffer_geom(obj):
|
2020-04-27 07:03:22 +00:00
|
|
|
try:
|
2019-12-23 20:59:01 +00:00
|
|
|
new_obj = []
|
|
|
|
for g in obj:
|
|
|
|
new_obj.append(buffer_geom(g))
|
|
|
|
return new_obj
|
2020-04-27 07:03:22 +00:00
|
|
|
except TypeError:
|
2019-12-23 20:59:01 +00:00
|
|
|
try:
|
2019-12-29 23:18:56 +00:00
|
|
|
if factor is None:
|
|
|
|
return obj.buffer(distance, resolution=self.geo_steps_per_circle)
|
|
|
|
else:
|
|
|
|
return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
|
2019-12-23 20:59:01 +00:00
|
|
|
except AttributeError:
|
|
|
|
return obj
|
|
|
|
|
|
|
|
# buffer solid_geometry
|
|
|
|
for tool, tool_dict in list(self.tools.items()):
|
2019-12-29 23:18:56 +00:00
|
|
|
res = buffer_geom(tool_dict['solid_geometry'])
|
|
|
|
try:
|
|
|
|
__ = iter(res)
|
|
|
|
self.tools[tool]['solid_geometry'] = res
|
|
|
|
except TypeError:
|
|
|
|
self.tools[tool]['solid_geometry'] = [res]
|
|
|
|
if factor is None:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.tools[tool]['tooldia'] += distance
|
2019-12-29 23:18:56 +00:00
|
|
|
else:
|
2020-06-16 00:47:26 +00:00
|
|
|
self.tools[tool]['tooldia'] *= distance
|
2019-12-23 20:59:01 +00:00
|
|
|
|
|
|
|
self.create_geometry()
|