- modified NCC tool so for simple objects (single Polygon) the external object used as reference can have any shape, for the other types of objects the copper cleared area will be the convex hull of the reference object - modified the strings of the app wherever they contained the char seq <b> </b> so it is not included in the translated string
465 lines
18 KiB
Python
465 lines
18 KiB
Python
# ########################################################## ##
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
# http://flatcam.org #
|
|
# File Author: Marius Adrian Stanciu (c) #
|
|
# Date: 4/15/2019 #
|
|
# MIT Licence #
|
|
# ########################################################## ##
|
|
|
|
from FlatCAMTool import FlatCAMTool
|
|
|
|
from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner, FCButton, FCTable
|
|
from PyQt5 import QtGui, QtWidgets, QtCore
|
|
from PyQt5.QtCore import pyqtSignal
|
|
import re
|
|
import os
|
|
from datetime import datetime
|
|
from io import StringIO
|
|
|
|
import gettext
|
|
import FlatCAMTranslation as fcTranslate
|
|
import builtins
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
|
|
class PcbWizard(FlatCAMTool):
|
|
|
|
file_loaded = pyqtSignal(str, str)
|
|
|
|
toolName = _("PcbWizard Import Tool")
|
|
|
|
def __init__(self, app):
|
|
FlatCAMTool.__init__(self, app)
|
|
|
|
self.app = app
|
|
|
|
# Title
|
|
title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
|
|
title_label.setStyleSheet("""
|
|
QLabel
|
|
{
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
self.layout.addWidget(title_label)
|
|
|
|
self.layout.addWidget(QtWidgets.QLabel(""))
|
|
self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Load files")))
|
|
|
|
# Form Layout
|
|
form_layout = QtWidgets.QFormLayout()
|
|
self.layout.addLayout(form_layout)
|
|
|
|
self.excellon_label = QtWidgets.QLabel(_("Excellon file:"))
|
|
self.excellon_label.setToolTip(
|
|
_("Load the Excellon file.\n"
|
|
"Usually it has a .DRL extension")
|
|
)
|
|
self.excellon_brn = FCButton(_("Open"))
|
|
form_layout.addRow(self.excellon_label, self.excellon_brn)
|
|
|
|
self.inf_label = QtWidgets.QLabel(_("INF file:"))
|
|
self.inf_label.setToolTip(
|
|
_("Load the INF file.")
|
|
)
|
|
self.inf_btn = FCButton(_("Open"))
|
|
form_layout.addRow(self.inf_label, self.inf_btn)
|
|
|
|
self.tools_table = FCTable()
|
|
self.layout.addWidget(self.tools_table)
|
|
|
|
self.tools_table.setColumnCount(2)
|
|
self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
|
|
|
|
self.tools_table.horizontalHeaderItem(0).setToolTip(
|
|
_("Tool Number"))
|
|
self.tools_table.horizontalHeaderItem(1).setToolTip(
|
|
_("Tool diameter in file units."))
|
|
|
|
# start with apertures table hidden
|
|
self.tools_table.setVisible(False)
|
|
|
|
self.layout.addWidget(QtWidgets.QLabel(""))
|
|
self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Excellon format")))
|
|
# Form Layout
|
|
form_layout1 = QtWidgets.QFormLayout()
|
|
self.layout.addLayout(form_layout1)
|
|
|
|
# Integral part of the coordinates
|
|
self.int_entry = FCSpinner()
|
|
self.int_entry.set_range(1, 10)
|
|
self.int_label = QtWidgets.QLabel(_("Int. digits:"))
|
|
self.int_label.setToolTip(
|
|
_("The number of digits for the integral part of the coordinates.")
|
|
)
|
|
form_layout1.addRow(self.int_label, self.int_entry)
|
|
|
|
# Fractional part of the coordinates
|
|
self.frac_entry = FCSpinner()
|
|
self.frac_entry.set_range(1, 10)
|
|
self.frac_label = QtWidgets.QLabel(_("Frac. digits:"))
|
|
self.frac_label.setToolTip(
|
|
_("The number of digits for the fractional part of the coordinates.")
|
|
)
|
|
form_layout1.addRow(self.frac_label, self.frac_entry)
|
|
|
|
# Zeros suppression for coordinates
|
|
self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
|
|
{'label': _('TZ'), 'value': 'TZ'},
|
|
{'label': _('No Suppression'), 'value': 'D'}])
|
|
self.zeros_label = QtWidgets.QLabel(_("Zeros supp.:"))
|
|
self.zeros_label.setToolTip(
|
|
_("The type of zeros suppression used.\n"
|
|
"Can be of type:\n"
|
|
"- LZ = leading zeros are kept\n"
|
|
"- TZ = trailing zeros are kept\n"
|
|
"- No Suppression = no zero suppression")
|
|
)
|
|
form_layout1.addRow(self.zeros_label, self.zeros_radio)
|
|
|
|
# Units type
|
|
self.units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
|
|
{'label': _('MM'), 'value': 'METRIC'}])
|
|
self.units_label = QtWidgets.QLabel("<b>%s:</b>" % _('Units'))
|
|
self.units_label.setToolTip(
|
|
_("The type of units that the coordinates and tool\n"
|
|
"diameters are using. Can be INCH or MM.")
|
|
)
|
|
form_layout1.addRow(self.units_label, self.units_radio)
|
|
|
|
# Buttons
|
|
|
|
self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
|
|
self.import_button.setToolTip(
|
|
_("Import in FlatCAM an Excellon file\n"
|
|
"that store it's information's in 2 files.\n"
|
|
"One usually has .DRL extension while\n"
|
|
"the other has .INF extension.")
|
|
)
|
|
self.layout.addWidget(self.import_button)
|
|
|
|
self.layout.addStretch()
|
|
|
|
self.excellon_loaded = False
|
|
self.inf_loaded = False
|
|
self.process_finished = False
|
|
|
|
self.modified_excellon_file = ''
|
|
|
|
# ## Signals
|
|
self.excellon_brn.clicked.connect(self.on_load_excellon_click)
|
|
self.inf_btn.clicked.connect(self.on_load_inf_click)
|
|
self.import_button.clicked.connect(lambda: self.on_import_excellon(
|
|
excellon_fileobj=self.modified_excellon_file))
|
|
|
|
self.file_loaded.connect(self.on_file_loaded)
|
|
self.units_radio.activated_custom.connect(self.on_units_change)
|
|
|
|
self.units = 'INCH'
|
|
self.zeros = 'LZ'
|
|
self.integral = 2
|
|
self.fractional = 4
|
|
|
|
self.outname = 'file'
|
|
|
|
self.exc_file_content = None
|
|
self.tools_from_inf = {}
|
|
|
|
def run(self, toggle=False):
|
|
self.app.report_usage("PcbWizard Tool()")
|
|
|
|
if toggle:
|
|
# if the splitter is hidden, display it, else hide it but only if the current widget is the same
|
|
if self.app.ui.splitter.sizes()[0] == 0:
|
|
self.app.ui.splitter.setSizes([1, 1])
|
|
else:
|
|
try:
|
|
if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
|
|
self.app.ui.splitter.setSizes([0, 1])
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if self.app.ui.splitter.sizes()[0] == 0:
|
|
self.app.ui.splitter.setSizes([1, 1])
|
|
|
|
FlatCAMTool.run(self)
|
|
self.set_tool_ui()
|
|
|
|
self.app.ui.notebook.setTabText(2, _("PCBWizard Tool"))
|
|
|
|
def install(self, icon=None, separator=None, **kwargs):
|
|
FlatCAMTool.install(self, icon, separator, **kwargs)
|
|
|
|
def set_tool_ui(self):
|
|
self.units = 'INCH'
|
|
self.zeros = 'LZ'
|
|
self.integral = 2
|
|
self.fractional = 4
|
|
|
|
self.outname = 'file'
|
|
|
|
self.exc_file_content = None
|
|
self.tools_from_inf = {}
|
|
|
|
# ## Initialize form
|
|
self.int_entry.set_value(self.integral)
|
|
self.frac_entry.set_value(self.fractional)
|
|
self.zeros_radio.set_value(self.zeros)
|
|
self.units_radio.set_value(self.units)
|
|
|
|
self.excellon_loaded = False
|
|
self.inf_loaded = False
|
|
self.process_finished = False
|
|
self.modified_excellon_file = ''
|
|
|
|
self.build_ui()
|
|
|
|
def build_ui(self):
|
|
sorted_tools = []
|
|
|
|
if not self.tools_from_inf:
|
|
self.tools_table.setVisible(False)
|
|
else:
|
|
sort = []
|
|
for k, v in list(self.tools_from_inf.items()):
|
|
sort.append(int(k))
|
|
sorted_tools = sorted(sort)
|
|
n = len(sorted_tools)
|
|
self.tools_table.setRowCount(n)
|
|
|
|
tool_row = 0
|
|
for tool in sorted_tools:
|
|
tool_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool))
|
|
tool_id_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
self.tools_table.setItem(tool_row, 0, tool_id_item) # Tool name/id
|
|
|
|
tool_dia_item = QtWidgets.QTableWidgetItem(str(self.tools_from_inf[tool]))
|
|
tool_dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
self.tools_table.setItem(tool_row, 1, tool_dia_item)
|
|
tool_row += 1
|
|
|
|
self.tools_table.resizeColumnsToContents()
|
|
self.tools_table.resizeRowsToContents()
|
|
|
|
vertical_header = self.tools_table.verticalHeader()
|
|
vertical_header.hide()
|
|
self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
horizontal_header = self.tools_table.horizontalHeader()
|
|
# horizontal_header.setMinimumSectionSize(10)
|
|
# horizontal_header.setDefaultSectionSize(70)
|
|
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
|
|
|
self.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
self.tools_table.setSortingEnabled(False)
|
|
self.tools_table.setMinimumHeight(self.tools_table.getHeight())
|
|
self.tools_table.setMaximumHeight(self.tools_table.getHeight())
|
|
|
|
def update_params(self):
|
|
self.units = self.units_radio.get_value()
|
|
self.zeros = self.zeros_radio.get_value()
|
|
self.integral = self.int_entry.get_value()
|
|
self.fractional = self.frac_entry.get_value()
|
|
|
|
def on_units_change(self, val):
|
|
if val == 'INCH':
|
|
self.int_entry.set_value(2)
|
|
self.frac_entry.set_value(4)
|
|
else:
|
|
self.int_entry.set_value(3)
|
|
self.frac_entry.set_value(3)
|
|
|
|
def on_load_excellon_click(self):
|
|
"""
|
|
|
|
:return: None
|
|
"""
|
|
self.app.log.debug("on_load_excellon_click()")
|
|
|
|
_filter = "Excellon Files(*.DRL *.DRD *.TXT);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
|
|
filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.app.inform.emit(_("Open cancelled."))
|
|
else:
|
|
self.app.worker_task.emit({'fcn': self.load_excellon, 'params': [filename]})
|
|
|
|
def on_load_inf_click(self):
|
|
"""
|
|
|
|
:return: None
|
|
"""
|
|
self.app.log.debug("on_load_inf_click()")
|
|
|
|
_filter = "INF Files(*.INF);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
|
|
filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.app.inform.emit(_("Open cancelled."))
|
|
else:
|
|
self.app.worker_task.emit({'fcn': self.load_inf, 'params': [filename]})
|
|
|
|
def load_inf(self, filename):
|
|
self.app.log.debug("ToolPcbWizard.load_inf()")
|
|
|
|
with open(filename, 'r') as inf_f:
|
|
inf_file_content = inf_f.readlines()
|
|
|
|
tool_re = re.compile(r'^T(\d+)\s+(\d*\.?\d+)$')
|
|
format_re = re.compile(r'^(\d+)\.?(\d+)\s*format,\s*(inches|metric)?,\s*(absolute|incremental)?.*$')
|
|
|
|
for eline in inf_file_content:
|
|
# Cleanup lines
|
|
eline = eline.strip(' \r\n')
|
|
|
|
match = tool_re.search(eline)
|
|
if match:
|
|
tool = int(match.group(1))
|
|
dia = float(match.group(2))
|
|
# if dia < 0.1:
|
|
# # most likely the file is in INCH
|
|
# self.units_radio.set_value('INCH')
|
|
|
|
self.tools_from_inf[tool] = dia
|
|
continue
|
|
match = format_re.search(eline)
|
|
if match:
|
|
self.integral = int(match.group(1))
|
|
self.fractional = int(match.group(2))
|
|
units = match.group(3)
|
|
if units == 'inches':
|
|
self.units = 'INCH'
|
|
else:
|
|
self.units = 'METRIC'
|
|
self.units_radio.set_value(self.units)
|
|
self.int_entry.set_value(self.integral)
|
|
self.frac_entry.set_value(self.fractional)
|
|
|
|
if not self.tools_from_inf:
|
|
self.app.inform.emit(_("[ERROR] The INF file does not contain the tool table.\n"
|
|
"Try to open the Excellon file from File -> Open -> Excellon\n"
|
|
"and edit the drill diameters manually."))
|
|
return "fail"
|
|
|
|
self.file_loaded.emit('inf', filename)
|
|
|
|
def load_excellon(self, filename):
|
|
with open(filename, 'r') as exc_f:
|
|
self.exc_file_content = exc_f.readlines()
|
|
|
|
self.file_loaded.emit("excellon", filename)
|
|
|
|
def on_file_loaded(self, signal, filename):
|
|
self.build_ui()
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
|
|
if signal == 'inf':
|
|
self.inf_loaded = True
|
|
self.tools_table.setVisible(True)
|
|
self.app.inform.emit(_("[success] PcbWizard .INF file loaded."))
|
|
elif signal == 'excellon':
|
|
self.excellon_loaded = True
|
|
self.outname = os.path.split(str(filename))[1]
|
|
self.app.inform.emit(_("[success] Main PcbWizard Excellon file loaded."))
|
|
|
|
if self.excellon_loaded and self.inf_loaded:
|
|
self.update_params()
|
|
excellon_string = ''
|
|
for line in self.exc_file_content:
|
|
excellon_string += line
|
|
if 'M48' in line:
|
|
header = ';EXCELLON RE-GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
|
|
(str(self.app.version), str(self.app.version_date))
|
|
header += ';Created on : %s' % time_str + '\n'
|
|
header += ';FILE_FORMAT={integral}:{fractional}\n'.format(integral=self.integral,
|
|
fractional=self.fractional)
|
|
header += '{units},{zeros}\n'.format(units=self.units, zeros=self.zeros)
|
|
for k, v in self.tools_from_inf.items():
|
|
header += 'T{tool}C{dia}\n'.format(tool=int(k), dia=float(v))
|
|
excellon_string += header
|
|
self.modified_excellon_file = StringIO(excellon_string)
|
|
self.process_finished = True
|
|
|
|
# Register recent file
|
|
self.app.defaults["global_last_folder"] = os.path.split(str(filename))[0]
|
|
|
|
def on_import_excellon(self, signal=None, excellon_fileobj=None):
|
|
self.app.log.debug("import_2files_excellon()")
|
|
|
|
# How the object should be initialized
|
|
def obj_init(excellon_obj, app_obj):
|
|
# self.progress.emit(20)
|
|
|
|
try:
|
|
ret = excellon_obj.parse_file(file_obj=excellon_fileobj)
|
|
if ret == "fail":
|
|
app_obj.log.debug("Excellon parsing failed.")
|
|
app_obj.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
|
|
return "fail"
|
|
except IOError:
|
|
app_obj.inform.emit(_("[ERROR_NOTCL] Cannot parse file: %s") % self.outname)
|
|
app_obj.log.debug("Could not import Excellon object.")
|
|
app_obj.progress.emit(0)
|
|
return "fail"
|
|
except Exception as e:
|
|
app_obj.log.debug("PcbWizard.on_import_excellon().obj_init() %s" % str(e))
|
|
msg = _("[ERROR_NOTCL] An internal error has occurred. See shell.\n")
|
|
msg += app_obj.traceback.format_exc()
|
|
app_obj.inform.emit(msg)
|
|
return "fail"
|
|
|
|
ret = excellon_obj.create_geometry()
|
|
if ret == 'fail':
|
|
app_obj.log.debug("Could not create geometry for Excellon object.")
|
|
return "fail"
|
|
app_obj.progress.emit(100)
|
|
for tool in excellon_obj.tools:
|
|
if excellon_obj.tools[tool]['solid_geometry']:
|
|
return
|
|
app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % name)
|
|
return "fail"
|
|
|
|
if excellon_fileobj is not None and excellon_fileobj != '':
|
|
if self.process_finished:
|
|
with self.app.proc_container.new(_("Importing Excellon.")):
|
|
|
|
# Object name
|
|
name = self.outname
|
|
|
|
ret_val = self.app.new_object("excellon", name, obj_init, autoselected=False)
|
|
if ret_val == 'fail':
|
|
self.app.inform.emit(_('[ERROR_NOTCL] Import Excellon file failed.'))
|
|
return
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("excellon", name)
|
|
|
|
# GUI feedback
|
|
self.app.inform.emit(_("[success] Imported: %s") % name)
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
|
else:
|
|
self.app.inform.emit(_('[WARNING_NOTCL] Excellon merging is in progress. Please wait...'))
|
|
else:
|
|
self.app.inform.emit(_('[ERROR_NOTCL] The imported Excellon file is None.'))
|