diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0a4bdf55..f567029b 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2006,6 +2006,9 @@ class App(QtCore.QObject): self.image_tool = ToolImage(self) self.image_tool.install(icon=QtGui.QIcon('share/image32.png'), pos=self.ui.menufileimport, separator=True) + self.pcb_wizard_tool = PcbWizard(self) + self.pcb_wizard_tool.install(icon=QtGui.QIcon('share/drill32.png'), pos=self.ui.menufileimport, + separator=True) self.log.debug("Tools are installed.") @@ -7081,7 +7084,7 @@ class App(QtCore.QObject): # self.progress.emit(20) try: - ret = excellon_obj.parse_file(filename) + ret = excellon_obj.parse_file(filename=filename) if ret == "fail": log.debug("Excellon parsing failed.") self.inform.emit(_("[ERROR_NOTCL] This is not Excellon file.")) diff --git a/README.md b/README.md index 80f9c49d..08e0ee9d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ CAD program, and create G-Code for Isolation routing. ================================================= +15.04.2019 + +- working on a new tool to process automatically PcbWizard Excellon files which are generated in 2 files + 14.04.2019 - Gerber Editor: Remade the processing of 'clear_geometry' (geometry generated by polygons made with Gerber LPC command) to work if more than one such polygon exists diff --git a/camlib.py b/camlib.py index 6f46b161..7676bee1 100644 --- a/camlib.py +++ b/camlib.py @@ -3837,7 +3837,7 @@ class Excellon(Geometry): # Repeating command self.repeat_re = re.compile(r'R(\d+)') - def parse_file(self, filename): + def parse_file(self, filename=None, file_obj=None): """ Reads the specified file as array of lines as passes it to ``parse_lines()``. @@ -3846,9 +3846,15 @@ class Excellon(Geometry): :type filename: str :return: None """ - efile = open(filename, 'r') - estr = efile.readlines() - efile.close() + 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: diff --git a/flatcamGUI/GUIElements.py b/flatcamGUI/GUIElements.py index e30a2f9b..c7b724f5 100644 --- a/flatcamGUI/GUIElements.py +++ b/flatcamGUI/GUIElements.py @@ -27,7 +27,7 @@ EDIT_SIZE_HINT = 70 class RadioSet(QtWidgets.QWidget): - activated_custom = QtCore.pyqtSignal() + activated_custom = QtCore.pyqtSignal(str) def __init__(self, choices, orientation='horizontal', parent=None, stretch=None): """ @@ -72,7 +72,8 @@ class RadioSet(QtWidgets.QWidget): radio = self.sender() if radio.isChecked(): self.group_toggle_fn() - self.activated_custom.emit() + ret_val = str(self.get_value()) + self.activated_custom.emit(ret_val) return def get_value(self): diff --git a/flatcamTools/ToolPcbWizard.py b/flatcamTools/ToolPcbWizard.py new file mode 100644 index 00000000..d990140d --- /dev/null +++ b/flatcamTools/ToolPcbWizard.py @@ -0,0 +1,415 @@ +############################################################ +# 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 + +import gettext +import FlatCAMTranslation as fcTranslate + +fcTranslate.apply_language('strings') +import builtins +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("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("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': 'L'}, + {'label': 'TZ', 'value': 'T'}, + {'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("%s:" % _('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 + + ## 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(self.on_import_excellon) + self.file_loaded.connect(self.on_file_loaded) + self.units_radio.activated_custom.connect(self.on_units_change) + + self.units = 'INCH' + self.zeros = 'L' + 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): + ## 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.build_ui() + + def build_ui(self): + sorted_tools = [] + + if not self.tools_from_inf: + self.tools_table.setRowCount(1) + 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': [self, 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+)$') + + 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 + + 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.tools_table.setVisible(True) + 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() + + if signal == 'inf': + self.inf_loaded = True + elif signal == 'excellon': + self.excellon_loaded = True + + if self.excellon_loaded and self.inf_loaded: + pass + + + # Register recent file + self.app.defaults["global_last_folder"] = os.path.split(str(filename))[0] + + def on_import_excellon(self, signal, excellon_fileobj): + 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: + 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 self.process_finished: + with self.app.proc_container.new(_("Importing Excellon.")): + + # Object name + name = self.outname + + ret = self.app.new_object("excellon", name, obj_init, autoselected=False) + if ret == '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] Opened: %s") % name) + else: + self.app.inform.emit(_('[WARNING_NOTCL] Excellon merging is in progress. Please wait...')) + diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index cd9dd56c..6119a4c1 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -14,5 +14,6 @@ from flatcamTools.ToolPaint import ToolPaint from flatcamTools.ToolNonCopperClear import NonCopperClear from flatcamTools.ToolTransform import ToolTransform from flatcamTools.ToolSolderPaste import SolderPaste +from flatcamTools.ToolPcbWizard import PcbWizard from flatcamTools.ToolShell import FCShell