Przeglądaj źródła

Merged in marius_stanciu/flatcam_beta/Beta (pull request #248)

Big Beta 8.99 attempt
Marius Stanciu 6 lat temu
rodzic
commit
7b638acfc5
100 zmienionych plików z 15684 dodań i 5982 usunięć
  1. 1075 871
      FlatCAMApp.py
  2. 1259 2
      FlatCAMCommon.py
  3. 378 257
      FlatCAMObj.py
  4. 11 11
      FlatCAMPostProc.py
  5. 51 3
      FlatCAMTool.py
  6. 2 2
      FlatCAMWorker.py
  7. 7 3
      ObjectCollection.py
  8. 337 1
      README.md
  9. 294 271
      camlib.py
  10. 64 34
      flatcamEditors/FlatCAMExcEditor.py
  11. 129 137
      flatcamEditors/FlatCAMGeoEditor.py
  12. 325 311
      flatcamEditors/FlatCAMGrbEditor.py
  13. 29 14
      flatcamEditors/FlatCAMTextEditor.py
  14. 223 443
      flatcamGUI/FlatCAMGUI.py
  15. 424 33
      flatcamGUI/GUIElements.py
  16. 260 159
      flatcamGUI/ObjectUI.py
  17. 114 70
      flatcamGUI/PlotCanvas.py
  18. 113 4
      flatcamGUI/PlotCanvasLegacy.py
  19. 320 133
      flatcamGUI/PreferencesUI.py
  20. 7 4
      flatcamGUI/VisPyPatches.py
  21. 4 2
      flatcamParsers/ParseDXF.py
  22. 111 104
      flatcamParsers/ParseExcellon.py
  23. 111 18
      flatcamParsers/ParseGerber.py
  24. 13 6
      flatcamParsers/ParseSVG.py
  25. 18 5
      flatcamTools/ToolCalculators.py
  26. 1171 0
      flatcamTools/ToolCalibration.py
  27. 1563 0
      flatcamTools/ToolCopperThieving.py
  28. 158 84
      flatcamTools/ToolCutOut.py
  29. 63 35
      flatcamTools/ToolDblSided.py
  30. 12 6
      flatcamTools/ToolDistance.py
  31. 4 4
      flatcamTools/ToolDistanceMin.py
  32. 920 0
      flatcamTools/ToolFiducials.py
  33. 605 63
      flatcamTools/ToolFilm.py
  34. 78 3
      flatcamTools/ToolImage.py
  35. 13 13
      flatcamTools/ToolMove.py
  36. 158 111
      flatcamTools/ToolNonCopperClear.py
  37. 36 4
      flatcamTools/ToolOptimal.py
  38. 3 2
      flatcamTools/ToolPDF.py
  39. 226 175
      flatcamTools/ToolPaint.py
  40. 24 7
      flatcamTools/ToolPanelize.py
  41. 1 0
      flatcamTools/ToolPcbWizard.py
  42. 267 49
      flatcamTools/ToolProperties.py
  43. 886 0
      flatcamTools/ToolQRCode.py
  44. 24 4
      flatcamTools/ToolRulesCheck.py
  45. 128 38
      flatcamTools/ToolSolderPaste.py
  46. 46 17
      flatcamTools/ToolSub.py
  47. 2 2
      flatcamTools/ToolTransform.py
  48. 5 0
      flatcamTools/__init__.py
  49. BIN
      locale/de/LC_MESSAGES/strings.mo
  50. 343 270
      locale/de/LC_MESSAGES/strings.po
  51. BIN
      locale/en/LC_MESSAGES/strings.mo
  52. 321 279
      locale/en/LC_MESSAGES/strings.po
  53. BIN
      locale/es/LC_MESSAGES/strings.mo
  54. 377 265
      locale/es/LC_MESSAGES/strings.po
  55. BIN
      locale/fr/LC_MESSAGES/strings.mo
  56. 376 264
      locale/fr/LC_MESSAGES/strings.po
  57. BIN
      locale/pt_BR/LC_MESSAGES/strings.mo
  58. 375 264
      locale/pt_BR/LC_MESSAGES/strings.po
  59. BIN
      locale/ro/LC_MESSAGES/strings.mo
  60. 335 270
      locale/ro/LC_MESSAGES/strings.po
  61. BIN
      locale/ru/LC_MESSAGES/strings.mo
  62. 407 270
      locale/ru/LC_MESSAGES/strings.po
  63. 428 354
      locale_template/strings.pot
  64. 24 9
      make_freezed.py
  65. 225 0
      preprocessors/Berta_CNC.py
  66. 157 0
      preprocessors/ISEL_CNC.py
  67. 9 6
      preprocessors/Paste_1.py
  68. 15 10
      preprocessors/Repetier.py
  69. 1 1
      preprocessors/Roland_MDX_20.py
  70. 5 7
      preprocessors/Toolchange_Custom.py
  71. 5 7
      preprocessors/Toolchange_Probe_MACH3.py
  72. 5 10
      preprocessors/Toolchange_manual.py
  73. 0 0
      preprocessors/__init__.py
  74. 27 20
      preprocessors/default.py
  75. 31 25
      preprocessors/grbl_11.py
  76. 2 2
      preprocessors/grbl_laser.py
  77. 1 1
      preprocessors/hpgl.py
  78. 5 7
      preprocessors/line_xyz.py
  79. 39 30
      preprocessors/marlin.py
  80. 6 4
      requirements.txt
  81. 8 33
      setup_ubuntu.sh
  82. BIN
      share/copperfill16.png
  83. BIN
      share/copperfill32.png
  84. BIN
      share/database32.png
  85. BIN
      share/fiducials_32.png
  86. BIN
      share/qrcode32.png
  87. 2 2
      tclCommands/TclCommandAddCircle.py
  88. 1 1
      tclCommands/TclCommandAddPolygon.py
  89. 2 1
      tclCommands/TclCommandAddPolyline.py
  90. 2 2
      tclCommands/TclCommandAddRectangle.py
  91. 9 3
      tclCommands/TclCommandAlignDrill.py
  92. 2 1
      tclCommands/TclCommandAlignDrillGrid.py
  93. 6 2
      tclCommands/TclCommandBbox.py
  94. 3 3
      tclCommands/TclCommandBounds.py
  95. 1 1
      tclCommands/TclCommandClearShell.py
  96. 9 7
      tclCommands/TclCommandCncjob.py
  97. 21 17
      tclCommands/TclCommandCopperClear.py
  98. 9 4
      tclCommands/TclCommandCutout.py
  99. 2 1
      tclCommands/TclCommandDelete.py
  100. 16 14
      tclCommands/TclCommandDrillcncjob.py

Plik diff jest za duży
+ 1075 - 871
FlatCAMApp.py


+ 1259 - 2
FlatCAMCommon.py

@@ -1,10 +1,33 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 11/4/2019                                          #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox
+from camlib import to_dict
+
+import sys
+import webbrowser
+import json
+
+from copy import deepcopy
+from datetime import datetime
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
 
 
 
 
 class LoudDict(dict):
 class LoudDict(dict):
@@ -69,3 +92,1237 @@ class FCSignal:
         except ValueError:
         except ValueError:
             print('Warning: function %s not removed '
             print('Warning: function %s not removed '
                   'from signal %s' % (func, self))
                   'from signal %s' % (func, self))
+
+
+class BookmarkManager(QtWidgets.QWidget):
+
+    mark_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, storage, parent=None):
+        super(BookmarkManager, self).__init__(parent)
+
+        self.app = app
+
+        assert isinstance(storage, dict), "Storage argument is not a dictionary"
+
+        self.bm_dict = deepcopy(storage)
+
+        # Icon and title
+        # self.setWindowIcon(parent.app_icon)
+        # self.setWindowTitle(_("Bookmark Manager"))
+        # self.resize(600, 400)
+
+        # title = QtWidgets.QLabel(
+        #     "<font size=8><B>FlatCAM</B></font><BR>"
+        # )
+        # title.setOpenExternalLinks(True)
+
+        # layouts
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        table_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(table_hlay)
+
+        self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1])
+        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table_hlay.addWidget(self.table_widget)
+
+        self.table_widget.setColumnCount(3)
+        self.table_widget.setColumnWidth(0, 20)
+        self.table_widget.setHorizontalHeaderLabels(
+            [
+                '#',
+                _('Title'),
+                _('Web Link')
+            ]
+        )
+        self.table_widget.horizontalHeaderItem(0).setToolTip(
+            _("Index.\n"
+              "The rows in gray color will populate the Bookmarks menu.\n"
+              "The number of gray colored rows is set in Preferences."))
+        self.table_widget.horizontalHeaderItem(1).setToolTip(
+            _("Description of the link that is set as an menu action.\n"
+              "Try to keep it short because it is installed as a menu item."))
+        self.table_widget.horizontalHeaderItem(2).setToolTip(
+            _("Web Link. E.g: https://your_website.org "))
+
+        # pal = QtGui.QPalette()
+        # pal.setColor(QtGui.QPalette.Background, Qt.white)
+
+        # New Bookmark
+        new_vlay = QtWidgets.QVBoxLayout()
+        layout.addLayout(new_vlay)
+
+        new_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Bookmark"))
+        new_vlay.addWidget(new_title_lbl)
+
+        form0 = QtWidgets.QFormLayout()
+        new_vlay.addLayout(form0)
+
+        title_lbl = QtWidgets.QLabel('%s:' % _("Title"))
+        self.title_entry = FCEntry()
+        form0.addRow(title_lbl, self.title_entry)
+
+        link_lbl = QtWidgets.QLabel('%s:' % _("Web Link"))
+        self.link_entry = FCEntry()
+        self.link_entry.set_value('http://')
+        form0.addRow(link_lbl, self.link_entry)
+
+        # Buttons Layout
+        button_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(button_hlay)
+
+        add_entry_btn = FCButton(_("Add Entry"))
+        remove_entry_btn = FCButton(_("Remove Entry"))
+        export_list_btn = FCButton(_("Export List"))
+        import_list_btn = FCButton(_("Import List"))
+        # closebtn = QtWidgets.QPushButton(_("Close"))
+
+        # button_hlay.addStretch()
+        button_hlay.addWidget(add_entry_btn)
+        button_hlay.addWidget(remove_entry_btn)
+
+        button_hlay.addWidget(export_list_btn)
+        button_hlay.addWidget(import_list_btn)
+        # button_hlay.addWidget(closebtn)
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        add_entry_btn.clicked.connect(self.on_add_entry)
+        remove_entry_btn.clicked.connect(self.on_remove_entry)
+        export_list_btn.clicked.connect(self.on_export_bookmarks)
+        import_list_btn.clicked.connect(self.on_import_bookmarks)
+        self.title_entry.returnPressed.connect(self.on_add_entry)
+        self.link_entry.returnPressed.connect(self.on_add_entry)
+        # closebtn.clicked.connect(self.accept)
+
+        self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
+        self.build_bm_ui()
+
+    def build_bm_ui(self):
+
+        self.table_widget.setRowCount(len(self.bm_dict))
+
+        nr_crt = 0
+        sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0]))
+        for entry, bookmark in sorted_bookmarks:
+            row = nr_crt
+            nr_crt += 1
+
+            title = bookmark[0]
+            weblink = bookmark[1]
+
+            id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.table_widget.setItem(row, 0, id_item)  # Tool name/id
+
+            title_item = QtWidgets.QTableWidgetItem(title)
+            self.table_widget.setItem(row, 1, title_item)
+
+            weblink_txt = QtWidgets.QTextBrowser()
+            weblink_txt.setOpenExternalLinks(True)
+            weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame)
+            weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }")
+
+            weblink_txt.setHtml('<a href=%s>%s</a>' % (weblink, weblink))
+
+            self.table_widget.setCellWidget(row, 2, weblink_txt)
+
+            vertical_header = self.table_widget.verticalHeader()
+            vertical_header.hide()
+
+            horizontal_header = self.table_widget.horizontalHeader()
+            horizontal_header.setMinimumSectionSize(10)
+            horizontal_header.setDefaultSectionSize(70)
+            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+            horizontal_header.resizeSection(0, 20)
+            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.mark_table_rows_for_actions()
+
+        self.app.defaults["global_bookmarks"].clear()
+        for key, val in self.bm_dict.items():
+            self.app.defaults["global_bookmarks"][key] = deepcopy(val)
+
+    def on_add_entry(self, **kwargs):
+        """
+        Add a entry in the Bookmark Table and in the menu actions
+        :return: None
+        """
+        if 'title' in kwargs:
+            title = kwargs['title']
+        else:
+            title = self.title_entry.get_value()
+        if title == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Title entry is empty."))
+            return 'fail'
+
+        if 'link' is kwargs:
+            link = kwargs['link']
+        else:
+            link = self.link_entry.get_value()
+
+        if link == 'http://':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Web link entry is empty."))
+            return 'fail'
+
+        # if 'http' not in link or 'https' not in link:
+        #     link = 'http://' + link
+
+        for bookmark in self.bm_dict.values():
+            if title == bookmark[0] or link == bookmark[1]:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Either the Title or the Weblink already in the table."))
+                return 'fail'
+
+        # for some reason if the last char in the weblink is a slash it does not make the link clickable
+        # so I remove it
+        if link[-1] == '/':
+            link = link[:-1]
+        # add the new entry to storage
+        new_entry = len(self.bm_dict) + 1
+        self.bm_dict[str(new_entry)] = [title, link]
+
+        # add the link to the menu but only if it is within the set limit
+        bm_limit = int(self.app.defaults["global_bookmarks_limit"])
+        if len(self.bm_dict) < bm_limit:
+            act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks)
+            act.setText(title)
+            act.setIcon(QtGui.QIcon('share/link16.png'))
+            act.triggered.connect(lambda: webbrowser.open(link))
+            self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act)
+
+        self.app.inform.emit('[success] %s' % _("Bookmark added."))
+
+        # add the new entry to the bookmark manager table
+        self.build_bm_ui()
+
+    def on_remove_entry(self):
+        """
+        Remove an Entry in the Bookmark table and from the menu actions
+        :return:
+        """
+        index_list = []
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            index = QtCore.QPersistentModelIndex(model_index)
+            index_list.append(index)
+            title_to_remove = self.table_widget.item(model_index.row(), 1).text()
+
+            if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site':
+                self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed"))
+                self.build_bm_ui()
+                return
+            else:
+                for k, bookmark in list(self.bm_dict.items()):
+                    if title_to_remove == bookmark[0]:
+                        # remove from the storage
+                        self.bm_dict.pop(k, None)
+
+                        for act in self.app.ui.menuhelp_bookmarks.actions():
+                            if act.text() == title_to_remove:
+                                # disconnect the signal
+                                try:
+                                    act.triggered.disconnect()
+                                except TypeError:
+                                    pass
+                                # remove the action from the menu
+                                self.app.ui.menuhelp_bookmarks.removeAction(act)
+
+        # house keeping: it pays to have keys increased by one
+        new_key = 0
+        new_dict = dict()
+        for k, v in self.bm_dict.items():
+            # we start with key 1 so we can use the len(self.bm_dict)
+            # when adding bookmarks (keys in bm_dict)
+            new_key += 1
+            new_dict[str(new_key)] = v
+
+        self.bm_dict = deepcopy(new_dict)
+        new_dict.clear()
+
+        self.app.inform.emit('[success] %s' % _("Bookmark removed."))
+
+        # for index in index_list:
+        #     self.table_widget.model().removeRow(index.row())
+        self.build_bm_ui()
+
+    def on_export_bookmarks(self):
+        self.app.report_usage("on_export_bookmarks")
+        self.app.log.debug("on_export_bookmarks()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Bookmarks"),
+                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
+                                                                 l_save=str(self.app.get_last_save_folder()),
+                                                                 n=_("Bookmarks"),
+                                                                 date=date),
+                                                             filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+            except IOError:
+                self.app.log.debug('Creating a new bookmarks file ...')
+                f = open(filename, 'w')
+                f.close()
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load defaults file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load bookmarks file."))
+                return
+
+            # Save Bookmarks to a file
+            try:
+                with open(filename, "w") as f:
+                    for title, link in self.bm_dict.items():
+                        line2write = str(title) + ':' + str(link) + '\n'
+                        f.write(line2write)
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write bookmarks to file."))
+                return
+        self.app.inform.emit('[success] %s: %s' % (_("Exported bookmarks to"), filename))
+
+    def on_import_bookmarks(self):
+        self.app.log.debug("on_import_bookmarks()")
+
+        filter_ = "Text File (*.txt);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), filter=filter_)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks import cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    bookmarks = f.readlines()
+            except IOError:
+                self.app.log.error("Could not load bookmarks file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load bookmarks file."))
+                return
+
+            for line in bookmarks:
+                proc_line = line.replace(' ', '').partition(':')
+                self.on_add_entry(title=proc_line[0], link=proc_line[2])
+
+            self.app.inform.emit('[success] %s: %s' % (_("Imported Bookmarks from"), filename))
+
+    def mark_table_rows_for_actions(self):
+        for row in range(self.table_widget.rowCount()):
+            item_to_paint = self.table_widget.item(row, 0)
+            if row < self.app.defaults["global_bookmarks_limit"]:
+                item_to_paint.setBackground(QtGui.QColor('gray'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+            else:
+                item_to_paint.setBackground(QtGui.QColor('white'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+
+    def rebuild_actions(self):
+        # rebuild the storage to reflect the order of the lines
+        self.bm_dict.clear()
+        for row in range(self.table_widget.rowCount()):
+            title = self.table_widget.item(row, 1).text()
+            wlink = self.table_widget.cellWidget(row, 2).toPlainText()
+
+            entry = int(row) + 1
+            self.bm_dict.update(
+                {
+                    str(entry): [title, wlink]
+                }
+            )
+
+        self.app.install_bookmarks(book_dict=self.bm_dict)
+
+    # def accept(self):
+    #     self.rebuild_actions()
+    #     super().accept()
+
+    def closeEvent(self, QCloseEvent):
+        self.rebuild_actions()
+        super().closeEvent(QCloseEvent)
+
+
+class ToolsDB(QtWidgets.QWidget):
+
+    mark_tools_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, callback_on_edited, callback_on_tool_request, parent=None):
+        super(ToolsDB, self).__init__(parent)
+
+        self.app = app
+        self.decimals = 4
+        self.callback_app = callback_on_edited
+
+        self.on_tool_request = callback_on_tool_request
+
+        self.offset_item_options = ["Path", "In", "Out", "Custom"]
+        self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+
+        '''
+        dict to hold all the tools in the Tools DB
+        format:
+        {
+            tool_id: {
+                'name': 'new_tool'
+                'tooldia': self.app.defaults["geometry_cnctooldia"]
+                'offset': 'Path'
+                'offset_value': 0.0
+                'type':  _('Rough'),
+                'tool_type': 'C1'
+                'data': dict()
+            }
+        }
+        '''
+        self.db_tool_dict = dict()
+
+        # layouts
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        table_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(table_hlay)
+
+        self.table_widget = FCTable(drag_drop=True)
+        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table_hlay.addWidget(self.table_widget)
+
+        self.table_widget.setColumnCount(26)
+        # self.table_widget.setColumnWidth(0, 20)
+        self.table_widget.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Tool Name"),
+                _("Tool Dia"),
+                _("Tool Offset"),
+                _("Custom Offset"),
+                _("Tool Type"),
+                _("Tool Shape"),
+                _("Cut Z"),
+                _("MultiDepth"),
+                _("DPP"),
+                _("V-Dia"),
+                _("V-Angle"),
+                _("Travel Z"),
+                _("FR"),
+                _("FR Z"),
+                _("FR Rapids"),
+                _("Spindle Speed"),
+                _("Dwell"),
+                _("Dwelltime"),
+                _("Preprocessor"),
+                _("ExtraCut"),
+                _("Toolchange"),
+                _("Toolchange XY"),
+                _("Toolchange Z"),
+                _("Start Z"),
+                _("End Z"),
+            ]
+        )
+        self.table_widget.horizontalHeaderItem(0).setToolTip(
+            _("Tool Index."))
+        self.table_widget.horizontalHeaderItem(1).setToolTip(
+            _("Tool name.\n"
+              "This is not used in the app, it's function\n"
+              "is to serve as a note for the user."))
+        self.table_widget.horizontalHeaderItem(2).setToolTip(
+            _("Tool Diameter."))
+        self.table_widget.horizontalHeaderItem(3).setToolTip(
+            _("Tool Offset.\n"
+              "Can be of a few types:\n"
+              "Path = zero offset\n"
+              "In = offset inside by half of tool diameter\n"
+              "Out = offset outside by half of tool diameter\n"
+              "Custom = custom offset using the Custom Offset value"))
+        self.table_widget.horizontalHeaderItem(4).setToolTip(
+            _("Custom Offset.\n"
+              "A value to be used as offset from the current path."))
+        self.table_widget.horizontalHeaderItem(5).setToolTip(
+            _("Tool Type.\n"
+              "Can be:\n"
+              "Iso = isolation cut\n"
+              "Rough = rough cut, low feedrate, multiple passes\n"
+              "Finish = finishing cut, high feedrate"))
+        self.table_widget.horizontalHeaderItem(6).setToolTip(
+            _("Tool Shape. \n"
+              "Can be:\n"
+              "C1 ... C4 = circular tool with x flutes\n"
+              "B = ball tip milling tool\n"
+              "V = v-shape milling tool"))
+        self.table_widget.horizontalHeaderItem(7).setToolTip(
+            _("Cutting Depth.\n"
+              "The depth at which to cut into material."))
+        self.table_widget.horizontalHeaderItem(8).setToolTip(
+            _("Multi Depth.\n"
+              "Selecting this will allow cutting in multiple passes,\n"
+              "each pass adding a DPP parameter depth."))
+        self.table_widget.horizontalHeaderItem(9).setToolTip(
+            _("DPP. Depth per Pass.\n"
+              "The value used to cut into material on each pass."))
+        self.table_widget.horizontalHeaderItem(10).setToolTip(
+            _("V-Dia.\n"
+              "Diameter of the tip for V-Shape Tools."))
+        self.table_widget.horizontalHeaderItem(11).setToolTip(
+            _("V-Agle.\n"
+              "Angle at the tip for the V-Shape Tools."))
+        self.table_widget.horizontalHeaderItem(12).setToolTip(
+            _("Clearance Height.\n"
+              "Height at which the milling bit will travel between cuts,\n"
+              "above the surface of the material, avoiding all fixtures."))
+        self.table_widget.horizontalHeaderItem(13).setToolTip(
+            _("FR. Feedrate\n"
+              "The speed on XY plane used while cutting into material."))
+        self.table_widget.horizontalHeaderItem(14).setToolTip(
+            _("FR Z. Feedrate Z\n"
+              "The speed on Z plane."))
+        self.table_widget.horizontalHeaderItem(15).setToolTip(
+            _("FR Rapids. Feedrate Rapids\n"
+              "Speed used while moving as fast as possible.\n"
+              "This is used only by some devices that can't use\n"
+              "the G0 g-code command. Mostly 3D printers."))
+        self.table_widget.horizontalHeaderItem(16).setToolTip(
+            _("Spindle Speed.\n"
+              "If it's left empty it will not be used.\n"
+              "The speed of the spindle in RPM."))
+        self.table_widget.horizontalHeaderItem(17).setToolTip(
+            _("Dwell.\n"
+              "Check this if a delay is needed to allow\n"
+              "the spindle motor to reach it's set speed."))
+        self.table_widget.horizontalHeaderItem(18).setToolTip(
+            _("Dwell Time.\n"
+              "A delay used to allow the motor spindle reach it's set speed."))
+        self.table_widget.horizontalHeaderItem(19).setToolTip(
+            _("Preprocessor.\n"
+              "A selection of files that will alter the generated G-code\n"
+              "to fit for a number of use cases."))
+        self.table_widget.horizontalHeaderItem(20).setToolTip(
+            _("Extra Cut.\n"
+              "If checked, after a isolation is finished an extra cut\n"
+              "will be added where the start and end of isolation meet\n"
+              "such as that this point is covered by this extra cut to\n"
+              "ensure a complete isolation."))
+        self.table_widget.horizontalHeaderItem(21).setToolTip(
+            _("Toolchange.\n"
+              "It will create a toolchange event.\n"
+              "The kind of toolchange is determined by\n"
+              "the preprocessor file."))
+        self.table_widget.horizontalHeaderItem(22).setToolTip(
+            _("Toolchange XY.\n"
+              "A set of coordinates in the format (x, y).\n"
+              "Will determine the cartesian position of the point\n"
+              "where the tool change event take place."))
+        self.table_widget.horizontalHeaderItem(23).setToolTip(
+            _("Toolchange Z.\n"
+              "The position on Z plane where the tool change event take place."))
+        self.table_widget.horizontalHeaderItem(24).setToolTip(
+            _("Start Z.\n"
+              "If it's left empty it will not be used.\n"
+              "A position on Z plane to move immediately after job start."))
+        self.table_widget.horizontalHeaderItem(25).setToolTip(
+            _("End Z.\n"
+              "A position on Z plane to move immediately after job stop."))
+
+        # pal = QtGui.QPalette()
+        # pal.setColor(QtGui.QPalette.Background, Qt.white)
+
+        # New Bookmark
+        new_vlay = QtWidgets.QVBoxLayout()
+        layout.addLayout(new_vlay)
+
+        # new_tool_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Tool"))
+        # new_vlay.addWidget(new_tool_lbl, alignment=QtCore.Qt.AlignBottom)
+
+        self.buttons_frame = QtWidgets.QFrame()
+        self.buttons_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.buttons_frame)
+        self.buttons_box = QtWidgets.QHBoxLayout()
+        self.buttons_box.setContentsMargins(0, 0, 0, 0)
+        self.buttons_frame.setLayout(self.buttons_box)
+        self.buttons_frame.show()
+
+        add_entry_btn = FCButton(_("Add Tool to Tools DB"))
+        add_entry_btn.setToolTip(
+            _("Add a new tool in the Tools Database.\n"
+              "You can edit it after it is added.")
+        )
+        remove_entry_btn = FCButton(_("Remove Tool from Tools DB"))
+        remove_entry_btn.setToolTip(
+            _("Remove a selection of tools in the Tools Database.")
+        )
+        export_db_btn = FCButton(_("Export Tool DB"))
+        export_db_btn.setToolTip(
+            _("Save the Tools Database to a custom text file.")
+        )
+        import_db_btn = FCButton(_("Import Tool DB"))
+        import_db_btn.setToolTip(
+            _("Load the Tools Database information's from a custom text file.")
+        )
+        # button_hlay.addStretch()
+        self.buttons_box.addWidget(add_entry_btn)
+        self.buttons_box.addWidget(remove_entry_btn)
+
+        self.buttons_box.addWidget(export_db_btn)
+        self.buttons_box.addWidget(import_db_btn)
+        # self.buttons_box.addWidget(closebtn)
+
+        self.add_tool_from_db = FCButton(_("Add Tool from Tools DB"))
+        self.add_tool_from_db.setToolTip(
+            _("Add a new tool in the Tools Table of the\n"
+              "active Geometry object after selecting a tool\n"
+              "in the Tools Database.")
+        )
+        self.add_tool_from_db.hide()
+
+        self.cancel_tool_from_db = FCButton(_("Cancel"))
+        self.cancel_tool_from_db.hide()
+
+        hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(hlay)
+        hlay.addWidget(self.add_tool_from_db)
+        hlay.addWidget(self.cancel_tool_from_db)
+        hlay.addStretch()
+
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        add_entry_btn.clicked.connect(self.on_tool_add)
+        remove_entry_btn.clicked.connect(self.on_tool_delete)
+        export_db_btn.clicked.connect(self.on_export_tools_db_file)
+        import_db_btn.clicked.connect(self.on_import_tools_db_file)
+        # closebtn.clicked.connect(self.accept)
+
+        self.add_tool_from_db.clicked.connect(self.on_tool_requested_from_app)
+        self.cancel_tool_from_db.clicked.connect(self.on_cancel_tool)
+
+        self.setup_db_ui()
+
+    def setup_db_ui(self):
+        filename = self.app.data_path + '/tools_db.FlatConfig'
+
+        # load the database tools from the file
+        try:
+            with open(filename) as f:
+                tools = f.read()
+        except IOError:
+            self.app.log.error("Could not load tools DB file.")
+            self.app.inform.emit('[ERROR] %s' % _("Could not load Tools DB file."))
+            return
+
+        try:
+            self.db_tool_dict = json.loads(tools)
+        except Exception:
+            e = sys.exc_info()[0]
+            self.app.log.error(str(e))
+            self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+            return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+
+        self.build_db_ui()
+
+        self.table_widget.setupContextMenu()
+        self.table_widget.addContextMenu(
+            _("Add to DB"), self.on_tool_add, icon=QtGui.QIcon("share/plus16.png"))
+        self.table_widget.addContextMenu(
+            _("Copy from DB"), self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
+        self.table_widget.addContextMenu(
+            _("Delete from DB"), self.on_tool_delete, icon=QtGui.QIcon("share/delete32.png"))
+
+    def build_db_ui(self):
+        self.ui_disconnect()
+        self.table_widget.setRowCount(len(self.db_tool_dict))
+
+        nr_crt = 0
+        for toolid, dict_val in self.db_tool_dict.items():
+            row = nr_crt
+            nr_crt += 1
+
+            t_name = dict_val['name']
+            self.add_tool_table_line(row, name=t_name, widget=self.table_widget, tooldict=dict_val)
+
+            vertical_header = self.table_widget.verticalHeader()
+            vertical_header.hide()
+
+            horizontal_header = self.table_widget.horizontalHeader()
+            horizontal_header.setMinimumSectionSize(10)
+            horizontal_header.setDefaultSectionSize(70)
+
+            self.table_widget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+            for x in range(27):
+                self.table_widget.resizeColumnToContents(x)
+
+            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+            # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+            # horizontal_header.setSectionResizeMode(13, QtWidgets.QHeaderView.Fixed)
+
+            horizontal_header.resizeSection(0, 20)
+            # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+            # horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.ui_connect()
+
+    def add_tool_table_line(self, row, name, widget, tooldict):
+        data = tooldict['data']
+
+        nr_crt = row + 1
+        id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+        # id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        flags = id_item.flags() & ~QtCore.Qt.ItemIsEditable
+        id_item.setFlags(flags)
+        widget.setItem(row, 0, id_item)  # Tool name/id
+
+        tool_name_item = QtWidgets.QTableWidgetItem(name)
+        widget.setItem(row, 1, tool_name_item)
+
+        dia_item = FCDoubleSpinner()
+        dia_item.set_precision(self.decimals)
+        dia_item.setSingleStep(0.1)
+        dia_item.set_range(0.0, 9999.9999)
+        dia_item.set_value(float(tooldict['tooldia']))
+        widget.setCellWidget(row, 2, dia_item)
+
+        tool_offset_item = FCComboBox()
+        for item in self.offset_item_options:
+            tool_offset_item.addItem(item)
+        tool_offset_item.set_value(tooldict['offset'])
+        widget.setCellWidget(row, 3, tool_offset_item)
+
+        c_offset_item = FCDoubleSpinner()
+        c_offset_item.set_precision(self.decimals)
+        c_offset_item.setSingleStep(0.1)
+        c_offset_item.set_range(-9999.9999, 9999.9999)
+        c_offset_item.set_value(float(tooldict['offset_value']))
+        widget.setCellWidget(row, 4, c_offset_item)
+
+        tt_item = FCComboBox()
+        for item in self.type_item_options:
+            tt_item.addItem(item)
+        tt_item.set_value(tooldict['type'])
+        widget.setCellWidget(row, 5, tt_item)
+
+        tshape_item = FCComboBox()
+        for item in self.tool_type_item_options:
+            tshape_item.addItem(item)
+        tshape_item.set_value(tooldict['tool_type'])
+        widget.setCellWidget(row, 6, tshape_item)
+
+        cutz_item = FCDoubleSpinner()
+        cutz_item.set_precision(self.decimals)
+        cutz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            cutz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            cutz_item.set_range(-9999.9999, -0.0000)
+
+        cutz_item.set_value(float(data['cutz']))
+        widget.setCellWidget(row, 7, cutz_item)
+
+        multidepth_item = FCCheckBox()
+        multidepth_item.set_value(data['multidepth'])
+        widget.setCellWidget(row, 8, multidepth_item)
+
+        depth_per_pass_item = FCDoubleSpinner()
+        depth_per_pass_item.set_precision(self.decimals)
+        depth_per_pass_item.setSingleStep(0.1)
+        depth_per_pass_item.set_range(0.0, 9999.9999)
+        depth_per_pass_item.set_value(float(data['depthperpass']))
+        widget.setCellWidget(row, 9, depth_per_pass_item)
+
+        vtip_dia_item = FCDoubleSpinner()
+        vtip_dia_item.set_precision(self.decimals)
+        vtip_dia_item.setSingleStep(0.1)
+        vtip_dia_item.set_range(0.0, 9999.9999)
+        vtip_dia_item.set_value(float(data['vtipdia']))
+        widget.setCellWidget(row, 10, vtip_dia_item)
+
+        vtip_angle_item = FCDoubleSpinner()
+        vtip_angle_item.set_precision(self.decimals)
+        vtip_angle_item.setSingleStep(0.1)
+        vtip_angle_item.set_range(-360.0, 360.0)
+        vtip_angle_item.set_value(float(data['vtipangle']))
+        widget.setCellWidget(row, 11, vtip_angle_item)
+
+        travelz_item = FCDoubleSpinner()
+        travelz_item.set_precision(self.decimals)
+        travelz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            travelz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            travelz_item.set_range(0.0000, 9999.9999)
+
+        travelz_item.set_value(float(data['travelz']))
+        widget.setCellWidget(row, 12, travelz_item)
+
+        fr_item = FCDoubleSpinner()
+        fr_item.set_precision(self.decimals)
+        fr_item.set_range(0.0, 9999.9999)
+        fr_item.set_value(float(data['feedrate']))
+        widget.setCellWidget(row, 13, fr_item)
+
+        frz_item = FCDoubleSpinner()
+        frz_item.set_precision(self.decimals)
+        frz_item.set_range(0.0, 9999.9999)
+        frz_item.set_value(float(data['feedrate_z']))
+        widget.setCellWidget(row, 14, frz_item)
+
+        frrapids_item = FCDoubleSpinner()
+        frrapids_item.set_precision(self.decimals)
+        frrapids_item.set_range(0.0, 9999.9999)
+        frrapids_item.set_value(float(data['feedrate_rapid']))
+        widget.setCellWidget(row, 15, frrapids_item)
+
+        spindlespeed_item = QtWidgets.QTableWidgetItem(str(data['spindlespeed']) if data['spindlespeed'] else '')
+        widget.setItem(row, 16, spindlespeed_item)
+
+        dwell_item = FCCheckBox()
+        dwell_item.set_value(data['dwell'])
+        widget.setCellWidget(row, 17, dwell_item)
+
+        dwelltime_item = FCDoubleSpinner()
+        dwelltime_item.set_precision(self.decimals)
+        dwelltime_item.set_range(0.0, 9999.9999)
+        dwelltime_item.set_value(float(data['dwelltime']))
+        widget.setCellWidget(row, 18, dwelltime_item)
+
+        pp_item = FCComboBox()
+        for item in self.app.preprocessors:
+            pp_item.addItem(item)
+        pp_item.set_value(data['ppname_g'])
+        widget.setCellWidget(row, 19, pp_item)
+
+        ecut_item = FCCheckBox()
+        ecut_item.set_value(data['extracut'])
+        widget.setCellWidget(row, 20, ecut_item)
+
+        toolchange_item = FCCheckBox()
+        toolchange_item.set_value(data['toolchange'])
+        widget.setCellWidget(row, 21, toolchange_item)
+
+        toolchangexy_item = QtWidgets.QTableWidgetItem(str(data['toolchangexy']) if data['toolchangexy'] else '')
+        widget.setItem(row, 22, toolchangexy_item)
+
+        toolchangez_item = FCDoubleSpinner()
+        toolchangez_item.set_precision(self.decimals)
+        toolchangez_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            toolchangez_item.set_range(-9999.9999, 9999.9999)
+        else:
+            toolchangez_item.set_range(0.0000, 9999.9999)
+
+        toolchangez_item.set_value(float(data['toolchangez']))
+        widget.setCellWidget(row, 23, toolchangez_item)
+
+        startz_item = QtWidgets.QTableWidgetItem(str(data['startz']) if data['startz'] else '')
+        widget.setItem(row, 24, startz_item)
+
+        endz_item = FCDoubleSpinner()
+        endz_item.set_precision(self.decimals)
+        endz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            endz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            endz_item.set_range(0.0000, 9999.9999)
+
+        endz_item.set_value(float(data['endz']))
+        widget.setCellWidget(row, 25, endz_item)
+
+    def on_tool_add(self):
+        """
+        Add a tool in the DB Tool Table
+        :return: None
+        """
+        new_toolid = len(self.db_tool_dict) + 1
+
+        dict_elem = dict()
+        default_data = dict()
+
+        default_data.update({
+            "cutz": float(self.app.defaults["geometry_cutz"]),
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
+            "vtipdia": float(self.app.defaults["geometry_vtipdia"]),
+            "vtipangle": float(self.app.defaults["geometry_vtipangle"]),
+            "travelz": float(self.app.defaults["geometry_travelz"]),
+            "feedrate": float(self.app.defaults["geometry_feedrate"]),
+            "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
+            "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
+            "startz": self.app.defaults["geometry_startz"],
+            "endz": float(self.app.defaults["geometry_endz"])
+        })
+
+        dict_elem['name'] = 'new_tool'
+        dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
+        dict_elem['offset'] = 'Path'
+        dict_elem['offset_value'] = 0.0
+        dict_elem['type'] = _('Rough')
+        dict_elem['tool_type'] = 'C1'
+
+        dict_elem['data'] = default_data
+
+        self.db_tool_dict.update(
+            {
+                new_toolid: deepcopy(dict_elem)
+            }
+        )
+
+        # add the new entry to the Tools DB table
+        self.build_db_ui()
+        self.callback_on_edited()
+        self.app.inform.emit('[success] %s' % _("Tool added to DB."))
+
+    def on_tool_copy(self):
+        """
+        Copy a selection of Tools in the Tools DB table
+        :return:
+        """
+        new_tool_id = self.table_widget.rowCount() + 1
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            # index = QtCore.QPersistentModelIndex(model_index)
+            old_tool_id = self.table_widget.item(model_index.row(), 0).text()
+            new_tool_id += 1
+
+            for toolid, dict_val in list(self.db_tool_dict.items()):
+                if int(old_tool_id) == int(toolid):
+                    self.db_tool_dict.update({
+                        new_tool_id: deepcopy(dict_val)
+                    })
+
+        self.build_db_ui()
+        self.callback_on_edited()
+        self.app.inform.emit('[success] %s' % _("Tool copied from Tools DB."))
+
+    def on_tool_delete(self):
+        """
+        Delete a selection of Tools in the Tools DB table
+        :return:
+        """
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            # index = QtCore.QPersistentModelIndex(model_index)
+            toolname_to_remove = self.table_widget.item(model_index.row(), 0).text()
+
+            for toolid, dict_val in list(self.db_tool_dict.items()):
+                if int(toolname_to_remove) == int(toolid):
+                    # remove from the storage
+                    self.db_tool_dict.pop(toolid, None)
+
+        self.build_db_ui()
+        self.callback_on_edited()
+        self.app.inform.emit('[success] %s' % _("Tool removed from Tools DB."))
+
+    def on_export_tools_db_file(self):
+        self.app.report_usage("on_export_tools_db_file")
+        self.app.log.debug("on_export_tools_db_file()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Tools Database"),
+                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
+                                                                 l_save=str(self.app.get_last_save_folder()),
+                                                                 n=_("Tools_Database"),
+                                                                 date=date),
+                                                             filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM Tools DB export cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+            except IOError:
+                self.app.log.debug('Creating a new Tools DB file ...')
+                f = open(filename, 'w')
+                f.close()
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+                return
+
+            # Save update options
+            try:
+                # Save Tools DB in a file
+                try:
+                    with open(filename, "w") as f:
+                        json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                except Exception as e:
+                    self.app.log.debug("App.on_save_tools_db() --> %s" % str(e))
+                    self.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                    return
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Exported Tools DB to"), filename))
+
+    def on_import_tools_db_file(self):
+        self.app.report_usage("on_import_tools_db_file")
+        self.app.log.debug("on_import_tools_db_file()")
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Tools DB"), filter=filter__)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM Tools DB import cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    tools_in_db = f.read()
+            except IOError:
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+                return
+
+            try:
+                self.db_tool_dict = json.loads(tools_in_db)
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+                return
+
+            self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+            self.build_db_ui()
+            self.callback_on_edited()
+
+    def on_save_tools_db(self, silent=False):
+        self.app.log.debug("ToolsDB.on_save_button() --> Saving Tools Database to file.")
+
+        filename = self.app.data_path + "/tools_db.FlatConfig"
+
+        # Preferences save, update the color of the Tools DB Tab text
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.app.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('black'))
+
+                # Save Tools DB in a file
+                try:
+                    f = open(filename, "w")
+                    json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                    f.close()
+                except Exception as e:
+                    self.app.log.debug("ToolsDB.on_save_tools_db() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                    return
+
+                if not silent:
+                    self.app.inform.emit('[success] %s' % _("Saved Tools DB."))
+
+    def ui_connect(self):
+        try:
+            try:
+                self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+            except (TypeError, AttributeError):
+                pass
+            self.table_widget.itemChanged.connect(self.callback_on_edited)
+        except AttributeError:
+            pass
+
+        for row in range(self.table_widget.rowCount()):
+            for col in range(self.table_widget.columnCount()):
+                # ComboBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).currentIndexChanged.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+                # CheckBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).toggled.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+                # SpinBox, DoubleSpinBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).valueChanged.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+    def ui_disconnect(self):
+        try:
+            self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+        except (TypeError, AttributeError):
+            pass
+
+        for row in range(self.table_widget.rowCount()):
+            for col in range(self.table_widget.columnCount()):
+                # ComboBox
+                try:
+                    self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                # CheckBox
+                try:
+                    self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                # SpinBox, DoubleSpinBox
+                try:
+                    self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+    def callback_on_edited(self):
+
+        # update the dictionary storage self.db_tool_dict
+        self.db_tool_dict.clear()
+        dict_elem = dict()
+        default_data = dict()
+
+        for row in range(self.table_widget.rowCount()):
+            new_toolid = row + 1
+            for col in range(self.table_widget.columnCount()):
+                column_header_text = self.table_widget.horizontalHeaderItem(col).text()
+                if column_header_text == 'Tool Name':
+                    dict_elem['name'] = self.table_widget.item(row, col).text()
+                elif column_header_text == 'Tool Dia':
+                    dict_elem['tooldia'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Offset':
+                    dict_elem['offset'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Custom Offset':
+                    dict_elem['offset_value'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Type':
+                    dict_elem['type'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Shape':
+                    dict_elem['tool_type'] = self.table_widget.cellWidget(row, col).get_value()
+                else:
+                    if column_header_text == 'Cut Z':
+                        default_data['cutz'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'MultiDepth':
+                        default_data['multidepth'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'DPP':
+                        default_data['depthperpass'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'V-Dia':
+                        default_data['vtipdia'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'V-Angle':
+                        default_data['vtipangle'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Travel Z':
+                        default_data['travelz'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR':
+                        default_data['feedrate'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR Z':
+                        default_data['feedrate_z'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR Rapids':
+                        default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Spindle Speed':
+                        default_data['spindlespeed'] = float(self.table_widget.item(row, col).text()) \
+                            if self.table_widget.item(row, col).text() is not '' else None
+                    elif column_header_text == 'Dwell':
+                        default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Dwelltime':
+                        default_data['dwelltime'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Preprocessor':
+                        default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'ExtraCut':
+                        default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Toolchange':
+                        default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Toolchange XY':
+                        default_data['toolchangexy'] = self.table_widget.item(row, col).text()
+                    elif column_header_text == 'Toolchange Z':
+                        default_data['toolchangez'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Start Z':
+                        default_data['startz'] = float(self.table_widget.item(row, col).text()) \
+                            if self.table_widget.item(row, col).text() is not '' else None
+                    elif column_header_text == 'End Z':
+                        default_data['endz'] = self.table_widget.cellWidget(row, col).get_value()
+
+            dict_elem['data'] = default_data
+            self.db_tool_dict.update(
+                {
+                    new_toolid: deepcopy(dict_elem)
+                }
+            )
+
+        self.callback_app()
+
+    def on_tool_requested_from_app(self):
+        if not self.table_widget.selectionModel().selectedRows():
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("No Tool/row selected in the Tools Database table"))
+            return
+
+        model_index_list = self.table_widget.selectionModel().selectedRows()
+        for model_index in model_index_list:
+            selected_row = model_index.row()
+            tool_uid = selected_row + 1
+            for key in self.db_tool_dict.keys():
+                if str(key) == str(tool_uid):
+                    selected_tool = self.db_tool_dict[key]
+                    self.on_tool_request(tool=selected_tool)
+
+    def on_cancel_tool(self):
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                wdg = self.app.ui.plot_tab_area.widget(idx)
+                wdg.deleteLater()
+                self.app.ui.plot_tab_area.removeTab(idx)
+        self.app.inform.emit('%s' % _("Cancelled adding tool from DB."))
+
+    def resize_new_tool_table_widget(self, min_size, max_size):
+        """
+        Resize the table widget responsible for adding new tool in the Tool Database
+
+        :param min_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+        :param max_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+        :return:
+        """
+        t_height = self.t_height
+        if max_size > min_size:
+            t_height = self.t_height + self.new_tool_table_widget.verticalScrollBar().height()
+
+        self.new_tool_table_widget.setMaximumHeight(t_height)
+
+    def closeEvent(self, QCloseEvent):
+        super().closeEvent(QCloseEvent)

Plik diff jest za duży
+ 378 - 257
FlatCAMObj.py


+ 11 - 11
FlatCAMPostProc.py

@@ -11,20 +11,20 @@ import os
 from abc import ABCMeta, abstractmethod
 from abc import ABCMeta, abstractmethod
 import math
 import math
 
 
-# module-root dictionary of postprocessors
+# module-root dictionary of preprocessors
 import FlatCAMApp
 import FlatCAMApp
 
 
-postprocessors = {}
+preprocessors = {}
 
 
 
 
 class ABCPostProcRegister(ABCMeta):
 class ABCPostProcRegister(ABCMeta):
-    # handles postprocessors registration on instantation
+    # handles preprocessors registration on instantiation
     def __new__(cls, clsname, bases, attrs):
     def __new__(cls, clsname, bases, attrs):
         newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
         newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
         if object not in bases:
         if object not in bases:
-            if newclass.__name__ in postprocessors:
-                FlatCAMApp.App.log.warning('Postprocessor %s has been overriden' % newclass.__name__)
-            postprocessors[newclass.__name__] = newclass()  # here is your register function
+            if newclass.__name__ in preprocessors:
+                FlatCAMApp.App.log.warning('Preprocessor %s has been overriden' % newclass.__name__)
+            preprocessors[newclass.__name__] = newclass()  # here is your register function
         return newclass
         return newclass
 
 
 
 
@@ -144,14 +144,14 @@ class FlatCAMPostProc_Tools(object, metaclass=ABCPostProcRegister):
         pass
         pass
 
 
 
 
-def load_postprocessors(app):
-    postprocessors_path_search = [os.path.join(app.data_path, 'postprocessors', '*.py'),
-                                  os.path.join('postprocessors', '*.py')]
+def load_preprocessors(app):
+    preprocessors_path_search = [os.path.join(app.data_path, 'preprocessors', '*.py'),
+                                  os.path.join('preprocessors', '*.py')]
     import glob
     import glob
-    for path_search in postprocessors_path_search:
+    for path_search in preprocessors_path_search:
         for file in glob.glob(path_search):
         for file in glob.glob(path_search):
             try:
             try:
                 SourceFileLoader('FlatCAMPostProcessor', file).load_module()
                 SourceFileLoader('FlatCAMPostProcessor', file).load_module()
             except Exception as e:
             except Exception as e:
                 app.log.error(str(e))
                 app.log.error(str(e))
-    return postprocessors
+    return preprocessors

+ 51 - 3
FlatCAMTool.py

@@ -9,6 +9,8 @@
 from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
 from PyQt5.QtCore import Qt
 from PyQt5.QtCore import Qt
 
 
+from shapely.geometry import Polygon
+
 
 
 class FlatCAMTool(QtWidgets.QWidget):
 class FlatCAMTool(QtWidgets.QWidget):
 
 
@@ -22,15 +24,15 @@ class FlatCAMTool(QtWidgets.QWidget):
         :param parent: Qt Parent
         :param parent: Qt Parent
         :return: FlatCAMTool
         :return: FlatCAMTool
         """
         """
-        QtWidgets.QWidget.__init__(self, parent)
+        self.app = app
+        self.decimals = app.decimals
 
 
+        QtWidgets.QWidget.__init__(self, parent)
         # self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
         # self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
 
 
         self.layout = QtWidgets.QVBoxLayout()
         self.layout = QtWidgets.QVBoxLayout()
         self.setLayout(self.layout)
         self.setLayout(self.layout)
 
 
-        self.app = app
-
         self.menuAction = None
         self.menuAction = None
 
 
     def install(self, icon=None, separator=None, shortcut=None, **kwargs):
     def install(self, icon=None, separator=None, shortcut=None, **kwargs):
@@ -90,3 +92,49 @@ class FlatCAMTool(QtWidgets.QWidget):
         self.app.ui.tool_scroll_area.widget().setObjectName(self.toolName)
         self.app.ui.tool_scroll_area.widget().setObjectName(self.toolName)
 
 
         self.show()
         self.show()
+
+    def draw_tool_selection_shape(self, old_coords, coords, **kwargs):
+        """
+
+        :param old_coords: old coordinates
+        :param coords: new coordinates
+        :return:
+        """
+
+        if 'color' in kwargs:
+            color = kwargs['color']
+        else:
+            color = self.app.defaults['global_sel_line']
+
+        if 'face_color' in kwargs:
+            face_color = kwargs['face_color']
+        else:
+            face_color = self.app.defaults['global_sel_fill']
+
+        if 'face_alpha' in kwargs:
+            face_alpha = kwargs['face_alpha']
+        else:
+            face_alpha = 0.3
+
+        x0, y0 = old_coords
+        x1, y1 = coords
+
+        pt1 = (x0, y0)
+        pt2 = (x1, y0)
+        pt3 = (x1, y1)
+        pt4 = (x0, y1)
+        sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
+        # color_t = Color(face_color)
+        # color_t.alpha = face_alpha
+
+        color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
+
+        self.app.tool_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
+                                 layer=0, tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.tool_shapes.redraw()
+
+    def delete_tool_selection_shape(self):
+        self.app.tool_shapes.clear()
+        self.app.tool_shapes.redraw()

+ 2 - 2
FlatCAMWorker.py

@@ -7,7 +7,7 @@
 # ########################################################## ##
 # ########################################################## ##
 
 
 from PyQt5 import QtCore
 from PyQt5 import QtCore
-# import traceback
+import traceback
 
 
 
 
 class Worker(QtCore.QObject):
 class Worker(QtCore.QObject):
@@ -61,7 +61,7 @@ class Worker(QtCore.QObject):
                 task['fcn'](*task['params'])
                 task['fcn'](*task['params'])
             except Exception as e:
             except Exception as e:
                 self.app.thread_exception.emit(e)
                 self.app.thread_exception.emit(e)
-                # print(traceback.format_exc())
+                print(traceback.format_exc())
                 # raise e
                 # raise e
             finally:
             finally:
                 self.task_completed.emit(self.name)
                 self.task_completed.emit(self.name)

+ 7 - 3
ObjectCollection.py

@@ -219,6 +219,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         "document": "share/notes16_1.png"
         "document": "share/notes16_1.png"
     }
     }
 
 
+    # will emit the name of the object that was just selected
+    item_selected = QtCore.pyqtSignal(str)
+
     root_item = None
     root_item = None
     # app = None
     # app = None
 
 
@@ -495,7 +498,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                 name += "_1"
                 name += "_1"
         obj.options["name"] = name
         obj.options["name"] = name
 
 
-        obj.set_ui(obj.ui_type())
+        obj.set_ui(obj.ui_type(decimals=self.app.decimals))
 
 
         # Required before appending (Qt MVC)
         # Required before appending (Qt MVC)
         group = self.group_items[obj.kind]
         group = self.group_items[obj.kind]
@@ -779,6 +782,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
 
         try:
         try:
             obj = current.indexes()[0].internalPointer().obj
             obj = current.indexes()[0].internalPointer().obj
+            self.item_selected.emit(obj.options['name'])
 
 
             if obj.kind == 'gerber':
             if obj.kind == 'gerber':
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
@@ -799,6 +803,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                     color='darkCyan', name=str(obj.options['name'])))
                     color='darkCyan', name=str(obj.options['name'])))
         except IndexError:
         except IndexError:
+            self.item_selected.emit('none')
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             self.app.inform.emit('')
             self.app.inform.emit('')
             try:
             try:
@@ -826,8 +831,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             try:
             try:
                 a_idx.build_ui()
                 a_idx.build_ui()
             except Exception as e:
             except Exception as e:
-                self.app.inform.emit('[ERROR] %s: %s' %
-                                     (_("Cause of error"), str(e)))
+                self.app.inform.emit('[ERROR] %s: %s' % (_("Cause of error"), str(e)))
                 raise
                 raise
 
 
     def get_list(self):
     def get_list(self):

+ 337 - 1
README.md

@@ -1,7 +1,7 @@
 FlatCAM: 2D Computer-Aided PCB Manufacturing
 FlatCAM: 2D Computer-Aided PCB Manufacturing
 =================================================
 =================================================
 
 
-(c) 2014-2019 Juan Pablo Caram
+(c) 2014-2016 Juan Pablo Caram
 
 
 FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
 FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
 Among other things, it can take a Gerber file generated by your favorite PCB
 Among other things, it can take a Gerber file generated by your favorite PCB
@@ -9,6 +9,342 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+9.12.2019 
+
+- updated the border for fit view on OpenGL graphic mode
+- Calibration Tool - added preferences values
+- Calibration Tool - more work on it
+- reverted this change: "selected object in Project used to ask twice for UI build" because it will not build the UI when a tab is closed for Document object and the object is selected
+- fixed issue after Geometry object edit; the GCode made from and edited object did not reflect the changes in the object
+- in Object UI, the Scale FCDoubleSpinner will no longer work for Return key press due of issues of unwanted scaling on focusOut event
+- in FlatCAMGeometry fixed the scale and offset methods to always process the self.solid_geometry
+- Calibration Tool - finished the calibrated object creation method
+- updated the POT file
+- fixed an error in the German PO file
+- updated the languages PO files
+
+8.12.2019
+
+- Calibrate Tool - rearranged the GUI
+- in Geometry UI made sure that the Label that points to the Tool parameters show clearly that those parameters apply only for the selected tool
+- fixed an small issue in Object UI
+- small fixes: selected object in Project used to ask twice for UI build; if scale factor in Object UI is 1 do nothing as there is no point in scaling with a factor of 1
+- in Geometry UI added a button that allow updating all the tools in the Tool Table with the current values in the UI form
+- updated Tcl commands to make use of either 0 or False for False value or 1 or True for True in case of a parameter with type Bool
+
+7.12.2019 
+
+- renamed Calibrate Excellon Tool to a simpler Calibrate Tool
+- Calibrate Tool - when generating verification GCode it will always load into an Editor from which it can be edited and/or saved. On save the editor will close.
+- updated the CNCJob and Drillcncjob Tcl Commands to use 0 and 1 as values for the parameters that are stated as of bool type, beside the normal keywords of False and True
+- Calibrate Tool - working on it
+
+6.12.2019
+
+- fixed the toggle_units() method so now the grid values are accurate to the decimal
+- cleaned up the Excellon parser and fixed some bugs (old and new); Excellon parser has it's own convert_units() method no longer inheriting from Geometry
+- in Excellon UI fixed bug that did not allow editing of the Offset Z parameter from the Tool table
+- in Properties Tool added new information's for the tools in the CNCjob objects
+- few bugs solved regarding the newly created empty objects
+- changed everywhere the name "postprocessor" with "preprocessor"
+- updated the preprocessor files in the toolchange section in order to avoid a graphical representation of travel lines glitch
+- fixed a GUI glitch in the Excellon tool table
+- added units to some of the parameters in the Properties Tool
+
+5.12.2019 
+
+- in NCC Tool, the new Geometry object that is created on copper clear now has the solid_geometry attribute where the geometry is stored not only in the obj.tools attribute
+- Copper Thieving Tool - added units label for the pattern plated area
+- Properties Tool - added a new parameter, the copper area which show the area of the copper features for the Gerber objects
+- Copper Thieving Tool - added a default value for the mask clearance when generating pattern plating mask
+- application wide change: introduced the precision parameters in Edit -> Preferences who will control how many decimals to use in the app parameters
+- changed the FCDoubleSpinner, FCSpinner and FCEntry GUI elements to allow passing an alignment value: left, right or center (not yet available in the app)
+- fixed the GUI of the Geometry Editor Tool Transform and adapted it to use the precision setting
+- updated Gerber Editor to use the precision setting and the Gerber Editor Transform Tool to use the FCDoubleSpinner GUI element
+- in Properties Tool added more information's regarding the Excellon tools, about travelled distance and job time; fixed issues when doing Properties on the CNCjob objects
+- TODO: I need to solve the mess in units conversion: it's too convoluted 
+
+4.12.2019 
+
+- made sure that if an older preferences file is detected then there are no errors and only the parameters that are currently active are loaded; the factory defaults file is deleted and recreated in the new format
+- in Preferences added a new button: 'Close' to close the Preferences window without saving
+- fixed bug in FCSpinner and FCDoubleSpinner GUI elements, that are now the main GUI element in FlatCAM, that made partial selection difficult
+- updated the Paint Tool in Geometry Editor to use the FCDoubleSpinner
+- added the possibility for suffix presence on the FCSpinner and FCDoubleSpinner GUI Elements
+- added the '%' symbol for overlap fields; I still need to divide the content by 100 to get the original (0 ... 1) value
+- fixed the overlap parameter all over the app to reflect the change to percentage
+- in Copper Thieving Tool added the display of the patterned plated area (approximate area) 
+- Copper Thieving Tool - updated the way plated area is calculated making it a bit more precise but still it is a bit bigger than the actual area
+- fixed the Copy Object function to copy also the source_file content
+- Copper Thieving Tool - when the clearance value for the pattern plating mask is negative it will be applied to the origin soldermask too
+- modified the GUI in all tools making the text of the buttons bold and adding a new button named Reset Tool which have to reset the tool GUI and variables (need to check all tools to see if happen)
+- when the Tool tab is in focus, clicking on canvas will no longer change the focus to Project tab
+- Copper Thieving Tool - when creating the pattern platting mask will make a new Gerber object with it
+- small fix in the GUI layout in Gerber Editor
+
+3.12.2019
+
+- in Preferences added an Apply button which apply the modified preferences but does not save to a file, minimizing the file IO operations; CTRL+S key combo does the Apply now.
+- updated some of the default values to metric, values that were missed previously
+- remade the Gerber Editor way to import an Gerber object into the editor in such a way to use the multiprocessing
+- various small fixes
+- fix for toggle grid lines updating canvas only after moving the mouse (hack, actually)
+- some changes in the UI layout in Cutout Tool
+- added some geometry parameters in Cutout Tool as a convenience, to be passed to the generated Geometry objects
+
+2.12.2019
+
+- fixed issue #343; updated the Image Tool
+- improvements in Importing SVG as Gerber - added an automatic source generation (it is not infallible)
+- a hack to import correctly the QRCode exported as SVG from FlatCAM
+- added 3 new tcl commands: export dxf, export excellon and export gerber
+- added a Cancel button in Tools DB when requesting to add a tool in the Geometry Tool Table
+- modified the default values for the METRIC system; the app now starts in the METRIC units since the majority of the world use the METRIC units system
+- small changes, updated the estimated release date
+- Tool Copper Thieving - added pattern plating mask generation feature
+
+28.11.2019
+
+- small fixes in NCC Tool and in the FlatCAMGeometry class
+
+27.11.2019
+
+- in Tool Film added the page size and page orientation in case of saving the film as PDF file
+- the application workspace has now a lot more options selectable in the Edit -> Preferences -> General -> GUI Preferences
+- updated the drawing of the workspace such that the application overall start time is improved and after first turn on of the workspace, toggling it will have no performance penalty
+- updated the workspace functions to work in Legacy(2D) graphic mode
+- adjusted the selection color transparency for the Legacy(2D) graphic mode because it was too transparent for the fill
+
+26.11.2019
+
+- updated the Film Tool to allow exporting PDF and PNG file (besides the SVG file)
+
+25.11.2019
+
+- In Gerber isolation changed the UI
+- in Gerber isolation added the option to selectively isolate only certain polygons
+- made some optimizations in FlatCAMGerber.isolate() method
+- updated the 'single' isolation of Gerber polygons to remove the polygon if clicked on it and it is already in the list of single polygons to be isolated
+- clicking to add a polygon when doing Single type isolation will add a blue shape marking the selected polygon, second click will remove that shape
+- fixed bugs in Paint Tool when painting single polygon
+- in Gerber isolation added the option to selectively isolate only certain polygons - made it to work for Legacy(2D) graphic mode
+- remade the Paint Tool - single polygon painting; now it can single paint a list of polygons that are clicked onto (right click will start the actual painting)
+
+23.11.2019
+
+- in Tool Fiducials added a new fiducial type: chess pattern
+- work in Calibrate Excellon Tool
+- fixed the line numbers in the TextPlainEdit to fit all digits of the line number; activated the line numbers for FlatCAMScript objects too
+- line numbers in the TextPlainEdit for the selected line are bold
+- made sure that the self.defaults dictionary is deepcopy-ed in the self.options dictionary
+- made sure that the units are read from the self.defaults and not from the GUI
+- added Robber Bar option to Copper Thieving Tool
+
+22.11.2019
+
+- Tool Fiducials - added GUI in Preferences and entries in self.defaults dict
+- Tool Fiducials - updated the source_file object for the modified Gerber files
+- working on adding line numbers to the TextPlainEdit
+- GCode view now has line numbers
+- solved a bug that made selection of objects on canvas impossible if there is an object of type FlatCAMScript or FlatCAMDocument opened
+
+21.11.2019
+
+- Tool Fiducials - finished the part with adding copper fiducials: manual and auto
+- Tool Fiducials - added choice of shapes: circular or non-standard cross
+- Tool Fiducials - finished the work on adding soldermask openings
+- Tool Fiducials - finished the tool
+- updated requirements.txt and setup_ubuntu.sh files
+
+20.11.2019
+
+- Tool Fiducials - added the GUI and the shortcut key
+- Tool Fiducials - updated the icon
+
+19.11.2019
+
+- removed the f-strings replacing them with the traditional string formatting due of not being supported by older versions of Python 3
+- fixed some TclCommands: MillDrills and OpenGerber
+- fixed bug in Tool Subtract that did not allow subtracting Gerber objects
+- starting to work on Tool Fiducials - created the file
+
+18.11.2019
+
+- finished the Dots and Squares options in the Copper Thieving Tool
+- working on the Lines option in Copper Thieving Tool
+- finished the Lines option in the Copper Thieving Tool; still have to add threading to maximize performance
+- finished Copper Thieving Tool improvements
+- working on the Calibrate Excellon Tool - remade the UI
+
+17.11.2019
+
+- optimized the storage of the Gerber mark shapes by making them one layer only
+- optimized the Distance Tool such that the distance utility geometry will be shown even when the mark shapes are plotted.
+- updated the make_freezed.py file to make sure that all the required files are included
+- updated the setup_ubuntu.sh to include the sudo command (courtesy of Krishna Torque on bitbucket)
+
+16.11.2019
+
+- fixed issue #341 that affected both postprocessors that have inlined feedrate: marlin and repetier. THe used feedrate was the Feedrate X-Y and instead had to be Feedrate Z.
+
+15.11.2019
+
+- added all the recognized extensions to the save dialog filters; latest extension used will be preselected next time a save operation occur
+- fixed issue #335. The FCDoubleSPinBox (and FCSpinBox) value was not used when the user entered data but just hovered away the mouse expecting the data to be already confirmed
+- converted setup_ubuntu.sh to Linux line endings
+
+14.11.2019
+
+- made sure that the 'default' postprocessor file is always loaded first such that this name is always first in the GUI comboboxes
+- added a class in GUIElements for a TextEdit box with line numbers and highlight
+
+13.11.2019
+
+- trying to improve the performance of View CNC Code command by using QPlainTextEdit; made the mods for it
+- when using the Find function in the TextEditor and the result reach the bottom of the document, the next find will be the first in the document (before it defaulted to the beginning of the document)
+- finished improving the show of text files in FlatCAM (CNC Code, Source files)
+- fixed an issue in the FlatCAMObj.FlatCAMGerber.convert_units() which needed to be updated after changes elsewhere
+
+12.11.2019
+
+- added two new postprocessor files for ISEL CNC and for BERTA CNC
+- clicking on a FCTable GUI element empty space will also clear the focus now
+
+11.11.2019
+
+- in Tools Database added a contextual menu to add/copy/delete tool; CTRL+C, DEL keys work too; key T for adding a tool is now only partially working
+- in Tools Database made the status bar messages show when adding/copying/deleting tools in DB
+- changed all Except statements that were single to except Exception as recommended in some PEP
+- renamed the Copper Fill Tool to Copper Thieving Tool as this is a more appropriate name; started to add ability for more types of copper thieving besides solid
+- fixed some issues recently introduced in ParseSVG
+- updated POT file
+- fixed GUI in 2Sided Tool
+- extending the Copper Thieving Tool - wip
+
+9.11.2019
+
+- fixed a new bug that did not allow to open the FlatCAM Preferences files by doubleclick in Windows
+- added a new feature: Tools Database for Geometry objects; resolved issue #308
+- added tooltips for the Tools Database table headers and buttons
+
+8.11.2019
+
+- updated the make file for frozen executable
+
+7.11.2019
+
+- added the '.ngc' file extension to the GCode Save file dialog filter
+- made the 'M2' Gcode command footer optional, default is False (can be set using the TclCommand: set_sys cncjob_footer True)
+- added a setting in Preferences to force the GCode output to have the Windows line-endings even for non-Windows OS's
+
+6.11.2019
+
+- the "CRTL+S" key combo when the Preferences Tab is in focus will save the Preferences instead of saving the Project
+- fixed bug in the Paint Tool that did not allow choosing a Paint Method that was not Standard
+- made sure that in the FlatCAMGeometry.merge() all the source data is deepcopy-ed in the final object
+- the font color of the Preferences tab will change to red if settings are not saved and it will revert to default when saved
+- fixed issue #333. The Geometry Editor Paint tool was not working and using it resulted in an error
+
+5.11.2019
+
+- added a new setting named 'Allow Machinist Unsafe Settings' that will allow the Travel Z and Cut Z to take both positive and negative values
+- fixed some issues when editing a multigeo geometry
+
+4.11.2019
+
+- wip
+- getting rid of all the Options GUI and related functions as it is no longer supported
+- updated the UI in Geometry UI
+- optimized the order of the defaults storage declaration and the update of the Preferences GUI from the defaults
+- started to add a Tool Database
+
+3.11.2019
+
+- fixed the V-shape tool diameter calculation in NCC Tool
+- in NCC Tool made the new tool dia (circular type) a parameter in Preferences
+- fixed a small issue with clicking in a disabled FCDoubleSpinner or FCSpinner still doing a selection
+
+30.10.2019
+
+- converted SolderPaste Tool to usage of SpinBoxes; changed the SolderPaste Tool UI in Preferences too
+- fixed a bug in SolderPaste Tool that did not allow to view the GCode
+
+29.10.2019
+
+- a bug fix in Geometry Object
+- fixed some missing properties in Tool Calculators
+
+28.10.2019
+
+- in Tools: Paint, NCC and Copper Fill, when using the Area Selection, now the selected areas will stay drawn as markers until the user click RMB
+- in legacy2D graphic engine, adding an utility geometry no longer draw the older ones, overwriting them
+- fixed some issues in the Gerber Editor (Aperture add was double adding an aperture)
+- converted Gerber Editor to usage of SpinBoxes
+- working on the Calibrate Excellon Tool
+- converted Excellon Editor to usage of SpinBoxes
+- Calibrate Excellon Tool: working on self.calculate_factors() method
+
+27.10.2019
+
+- Copper Fill Tool: some PEP8 corrections
+
+26.10.2019
+
+- fixed an error in the FCDoubleSpinner class when FlatCAM is run on system with locale that use the comma as decimal separator
+
+25.10.2019
+
+- QRCode Tool: added ability to add negative QRCodes (perhaps they can be isolated on copper?); added a clear area surrounding the QRCode in case it is dropped on a copper pour (region); fixed the Gerber export
+- QRCode Tool: all parameters are hard-coded for now
+- small update
+- fixed imports in all TclCommands
+- fixed the requirements.txt and setup_ubuntu.sh files
+- QRCode Tool: change the plot method parameter
+- QRCode Tool: added ability to save the generated QRCode as SVG file or PNG file
+- QRCode Tool: added the feature to save the PNG file with transparent background
+- QRCode Tool: added GUI category in Preferences window
+- QRCode Tool: shortcut key for this tool is now ALT+Q while PDF import Tool was relegated to CTRL+Q combo key shortcut
+- added a new FlatCAM Tool: Copper Fill Tool. It will pour copper into a Gerber filling all empty space with copper, at a clearance distance of the Gerber features
+- Copper Fill Tool: added possibility to select between a bounding box rectangular or convex hull when the reference is the geometry of the source Gerber object
+- Copper Fill Tool: cleanup on not regular tool exit
+- Copper Fill Tool: added GUI category in Edit -> Preferences window
+- QRCode Tool: added a selection limit parameter to control the selection shape vs utility geo
+
+24.10.2019
+
+- added some placeholder texts in the TextBoxes.
+- working on QRCode Tool; added the utility geometry and initial functional layout
+- working on QRCode Tool; finished adding the QRCode geometry to the selected Gerber object and also finished adding the 'follow' geometry needed when exporting the Gerber object as a Gerber file in addition to the 'solid' geometry in the obj.apertures
+- working on QRCode Tool; finished offsetting the geometry both in apertures and in solid_geometry; updated the source_file of the source object
+
+23.10.2019
+
+- QRCode Tool - a SVG object is generated and plotted on screen having the QRCode data
+- fixed an import error in Distance Tool
+- fixed the Toggle Grid Lines functionality
+
+22.10.2019
+
+- working on the Calibrate Excellon Tool
+- finished the GUI layout for the Calibrate Excellon Tool
+- start working on QRCode Tool - not working yet
+- start working on QRCode Tool - searching for alternatives
+
+21.10.2019
+
+- the context menu for the Tabs in notebook and PlotTabArea is launched now on right mouse click on tabs themselves
+- fixed an error when trying to view the source file and there is no object selected
+- updated the Objects menu signals so whenever an object is (de)selected in the Project Tab, it's state will reflect the (un)checked state of the actions in the Object menu
+- fixed issue in Gerber Object UI of not updating the value of CutZ entry on TipDia or TipAngle entries change. Fixed issue #324
+
+18.10.2019
+
+- fixed a small bug in BETA status change
+- updated the About FlatCAM window
+- reverted change in tool dia being able to take only positive values in Gerber Object UI
+- started to work to a new tool: Calibrate Excellon Tool
+- solved the issue #329
+
 18.10.2019
 18.10.2019
 
 
 - finished the update on the Google translated Spanish translation.
 - finished the update on the Google translated Spanish translation.

Plik diff jest za duży
+ 294 - 271
camlib.py


+ 64 - 34
flatcamEditors/FlatCAMExcEditor.py

@@ -9,7 +9,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 from PyQt5.QtCore import Qt, QSettings
 
 
 from camlib import distance, arc, FlatCAMRTreeStorage
 from camlib import distance, arc, FlatCAMRTreeStorage
-from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
+from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, RadioSet, FCSpinner
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamParsers.ParseExcellon import Excellon
 from flatcamParsers.ParseExcellon import Excellon
 import FlatCAMApp
 import FlatCAMApp
@@ -63,7 +63,7 @@ class FCDrillAdd(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_drill.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_drill.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -105,7 +105,7 @@ class FCDrillAdd(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
 
 
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
@@ -888,7 +888,7 @@ class FCDrillResize(FCShapeTool):
 
 
         try:
         try:
             new_dia = self.draw_app.resdrill_entry.get_value()
             new_dia = self.draw_app.resdrill_entry.get_value()
-        except:
+        except Exception:
             self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
             self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
                                           _("Resize drill(s) failed. Please enter a diameter for resize."))
                                           _("Resize drill(s) failed. Please enter a diameter for resize."))
             return
             return
@@ -1445,8 +1445,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app = app
         self.app = app
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
 
 
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
         # ## Current application units in Upper Case
         # ## Current application units in Upper Case
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         self.exc_edit_widget = QtWidgets.QWidget()
         self.exc_edit_widget = QtWidgets.QWidget()
         # ## Box for custom widgets
         # ## Box for custom widgets
@@ -1518,6 +1521,8 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         grid1 = QtWidgets.QGridLayout()
         grid1 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid1)
         self.tools_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
 
 
         addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('Tool Dia'))
         addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('Tool Dia'))
         addtool_entry_lbl.setToolTip(
         addtool_entry_lbl.setToolTip(
@@ -1525,8 +1530,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         )
 
 
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
-        self.addtool_entry = FCEntry()
-        self.addtool_entry.setValidator(QtGui.QDoubleValidator(0.0001, 99.9999, 4))
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.set_range(0.0000, 9999.9999)
+
         hlay.addWidget(self.addtool_entry)
         hlay.addWidget(self.addtool_entry)
 
 
         self.addtool_btn = QtWidgets.QPushButton(_('Add Tool'))
         self.addtool_btn = QtWidgets.QPushButton(_('Add Tool'))
@@ -1579,7 +1586,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         grid3.addWidget(res_entry_lbl, 0, 0)
         grid3.addWidget(res_entry_lbl, 0, 0)
 
 
         hlay2 = QtWidgets.QHBoxLayout()
         hlay2 = QtWidgets.QHBoxLayout()
-        self.resdrill_entry = LengthEntry()
+        self.resdrill_entry = FCDoubleSpinner()
+        self.resdrill_entry.set_precision(self.decimals)
+        self.resdrill_entry.set_range(0.0000, 9999.9999)
+
         hlay2.addWidget(self.resdrill_entry)
         hlay2.addWidget(self.resdrill_entry)
 
 
         self.resize_btn = QtWidgets.QPushButton(_('Resize'))
         self.resize_btn = QtWidgets.QPushButton(_('Resize'))
@@ -1633,7 +1643,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.drill_array_size_label.setToolTip(_("Specify how many drills to be in the array."))
         self.drill_array_size_label.setToolTip(_("Specify how many drills to be in the array."))
         self.drill_array_size_label.setMinimumWidth(100)
         self.drill_array_size_label.setMinimumWidth(100)
 
 
-        self.drill_array_size_entry = LengthEntry()
+        self.drill_array_size_entry = FCSpinner()
+        self.drill_array_size_entry.set_range(1, 9999)
         self.array_form.addRow(self.drill_array_size_label, self.drill_array_size_entry)
         self.array_form.addRow(self.drill_array_size_label, self.drill_array_size_entry)
 
 
         self.array_linear_frame = QtWidgets.QFrame()
         self.array_linear_frame = QtWidgets.QFrame()
@@ -1668,7 +1679,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         )
         self.drill_pitch_label.setMinimumWidth(100)
         self.drill_pitch_label.setMinimumWidth(100)
 
 
-        self.drill_pitch_entry = LengthEntry()
+        self.drill_pitch_entry = FCDoubleSpinner()
+        self.drill_pitch_entry.set_precision(self.decimals)
+        self.drill_pitch_entry.set_range(0.0000, 9999.9999)
+
         self.linear_form.addRow(self.drill_pitch_label, self.drill_pitch_entry)
         self.linear_form.addRow(self.drill_pitch_label, self.drill_pitch_entry)
 
 
         # Linear Drill Array angle
         # Linear Drill Array angle
@@ -1676,15 +1690,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.linear_angle_label.setToolTip(
         self.linear_angle_label.setToolTip(
            _("Angle at which the linear array is placed.\n"
            _("Angle at which the linear array is placed.\n"
              "The precision is of max 2 decimals.\n"
              "The precision is of max 2 decimals.\n"
-             "Min value is: -359.99 degrees.\n"
+             "Min value is: -360 degrees.\n"
              "Max value is:  360.00 degrees.")
              "Max value is:  360.00 degrees.")
         )
         )
         self.linear_angle_label.setMinimumWidth(100)
         self.linear_angle_label.setMinimumWidth(100)
 
 
         self.linear_angle_spinner = FCDoubleSpinner()
         self.linear_angle_spinner = FCDoubleSpinner()
-        self.linear_angle_spinner.set_precision(2)
+        self.linear_angle_spinner.set_precision(self.decimals)
         self.linear_angle_spinner.setSingleStep(1.0)
         self.linear_angle_spinner.setSingleStep(1.0)
-        self.linear_angle_spinner.setRange(-359.99, 360.00)
+        self.linear_angle_spinner.setRange(-360.00, 360.00)
         self.linear_form.addRow(self.linear_angle_label, self.linear_angle_spinner)
         self.linear_form.addRow(self.linear_angle_label, self.linear_angle_spinner)
 
 
         self.array_circular_frame = QtWidgets.QFrame()
         self.array_circular_frame = QtWidgets.QFrame()
@@ -1710,7 +1724,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.drill_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
         self.drill_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
         self.drill_angle_label.setMinimumWidth(100)
         self.drill_angle_label.setMinimumWidth(100)
 
 
-        self.drill_angle_entry = LengthEntry()
+        self.drill_angle_entry = FCDoubleSpinner()
+        self.drill_angle_entry.set_precision(self.decimals)
+        self.drill_angle_entry.setSingleStep(1.0)
+        self.drill_angle_entry.setRange(-360.00, 360.00)
+
         self.circular_form.addRow(self.drill_angle_label, self.drill_angle_entry)
         self.circular_form.addRow(self.drill_angle_label, self.drill_angle_entry)
 
 
         self.array_circular_frame.hide()
         self.array_circular_frame.hide()
@@ -1754,7 +1772,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         )
         self.slot_length_label.setMinimumWidth(100)
         self.slot_length_label.setMinimumWidth(100)
 
 
-        self.slot_length_entry = LengthEntry()
+        self.slot_length_entry = FCDoubleSpinner()
+        self.slot_length_entry.set_precision(self.decimals)
+        self.slot_length_entry.setSingleStep(0.1)
+        self.slot_length_entry.setRange(0.0000, 9999.9999)
+
         self.slot_form.addRow(self.slot_length_label, self.slot_length_entry)
         self.slot_form.addRow(self.slot_length_label, self.slot_length_entry)
 
 
         # Slot direction
         # Slot direction
@@ -1777,15 +1799,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_angle_label.setToolTip(
         self.slot_angle_label.setToolTip(
            _("Angle at which the slot is placed.\n"
            _("Angle at which the slot is placed.\n"
              "The precision is of max 2 decimals.\n"
              "The precision is of max 2 decimals.\n"
-             "Min value is: -359.99 degrees.\n"
+             "Min value is: -360 degrees.\n"
              "Max value is:  360.00 degrees.")
              "Max value is:  360.00 degrees.")
         )
         )
         self.slot_angle_label.setMinimumWidth(100)
         self.slot_angle_label.setMinimumWidth(100)
 
 
         self.slot_angle_spinner = FCDoubleSpinner()
         self.slot_angle_spinner = FCDoubleSpinner()
-        self.slot_angle_spinner.set_precision(2)
+        self.slot_angle_spinner.set_precision(self.decimals)
         self.slot_angle_spinner.setWrapping(True)
         self.slot_angle_spinner.setWrapping(True)
-        self.slot_angle_spinner.setRange(-359.99, 360.00)
+        self.slot_angle_spinner.setRange(-360.00, 360.00)
         self.slot_angle_spinner.setSingleStep(1.0)
         self.slot_angle_spinner.setSingleStep(1.0)
         self.slot_form.addRow(self.slot_angle_label, self.slot_angle_spinner)
         self.slot_form.addRow(self.slot_angle_label, self.slot_angle_spinner)
 
 
@@ -1835,7 +1857,9 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_array_size_label.setToolTip(_("Specify how many slots to be in the array."))
         self.slot_array_size_label.setToolTip(_("Specify how many slots to be in the array."))
         self.slot_array_size_label.setMinimumWidth(100)
         self.slot_array_size_label.setMinimumWidth(100)
 
 
-        self.slot_array_size_entry = LengthEntry()
+        self.slot_array_size_entry = FCSpinner()
+        self.slot_array_size_entry.set_range(0, 9999)
+
         self.slot_array_form.addRow(self.slot_array_size_label, self.slot_array_size_entry)
         self.slot_array_form.addRow(self.slot_array_size_label, self.slot_array_size_entry)
 
 
         self.slot_array_linear_frame = QtWidgets.QFrame()
         self.slot_array_linear_frame = QtWidgets.QFrame()
@@ -1870,7 +1894,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         )
         self.slot_array_pitch_label.setMinimumWidth(100)
         self.slot_array_pitch_label.setMinimumWidth(100)
 
 
-        self.slot_array_pitch_entry = LengthEntry()
+        self.slot_array_pitch_entry = FCDoubleSpinner()
+        self.slot_array_pitch_entry.set_precision(self.decimals)
+        self.slot_array_pitch_entry.setSingleStep(0.1)
+        self.slot_array_pitch_entry.setRange(0.0000, 9999.9999)
+
         self.slot_array_linear_form.addRow(self.slot_array_pitch_label, self.slot_array_pitch_entry)
         self.slot_array_linear_form.addRow(self.slot_array_pitch_label, self.slot_array_pitch_entry)
 
 
         # Linear Slot Array angle
         # Linear Slot Array angle
@@ -1878,15 +1906,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_array_linear_angle_label.setToolTip(
         self.slot_array_linear_angle_label.setToolTip(
             _("Angle at which the linear array is placed.\n"
             _("Angle at which the linear array is placed.\n"
               "The precision is of max 2 decimals.\n"
               "The precision is of max 2 decimals.\n"
-              "Min value is: -359.99 degrees.\n"
+              "Min value is: -360 degrees.\n"
               "Max value is:  360.00 degrees.")
               "Max value is:  360.00 degrees.")
         )
         )
         self.slot_array_linear_angle_label.setMinimumWidth(100)
         self.slot_array_linear_angle_label.setMinimumWidth(100)
 
 
         self.slot_array_linear_angle_spinner = FCDoubleSpinner()
         self.slot_array_linear_angle_spinner = FCDoubleSpinner()
-        self.slot_array_linear_angle_spinner.set_precision(2)
+        self.slot_array_linear_angle_spinner.set_precision(self.decimals)
         self.slot_array_linear_angle_spinner.setSingleStep(1.0)
         self.slot_array_linear_angle_spinner.setSingleStep(1.0)
-        self.slot_array_linear_angle_spinner.setRange(-359.99, 360.00)
+        self.slot_array_linear_angle_spinner.setRange(-360.00, 360.00)
         self.slot_array_linear_form.addRow(self.slot_array_linear_angle_label, self.slot_array_linear_angle_spinner)
         self.slot_array_linear_form.addRow(self.slot_array_linear_angle_label, self.slot_array_linear_angle_spinner)
 
 
         self.slot_array_circular_frame = QtWidgets.QFrame()
         self.slot_array_circular_frame = QtWidgets.QFrame()
@@ -1912,7 +1940,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_array_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
         self.slot_array_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
         self.slot_array_angle_label.setMinimumWidth(100)
         self.slot_array_angle_label.setMinimumWidth(100)
 
 
-        self.slot_array_angle_entry = LengthEntry()
+        self.slot_array_angle_entry = FCDoubleSpinner()
+        self.slot_array_angle_entry.set_precision(self.decimals)
+        self.slot_array_angle_entry.setSingleStep(1)
+        self.slot_array_angle_entry.setRange(-360.00, 360.00)
+
         self.slot_array_circular_form.addRow(self.slot_array_angle_label, self.slot_array_angle_entry)
         self.slot_array_circular_form.addRow(self.slot_array_angle_label, self.slot_array_angle_entry)
 
 
         self.slot_array_linear_angle_spinner.hide()
         self.slot_array_linear_angle_spinner.hide()
@@ -2050,9 +2082,6 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         self.complete = False
         self.complete = False
 
 
-        # Number of decimals used by tools in this class
-        self.decimals = 4
-
         def make_callback(thetool):
         def make_callback(thetool):
             def f():
             def f():
                 self.on_tool_select(thetool)
                 self.on_tool_select(thetool)
@@ -2070,7 +2099,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
         }
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
 
         for option in self.options:
         for option in self.options:
             if option in self.app.options:
             if option in self.app.options:
@@ -2121,7 +2150,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
     def set_ui(self):
     def set_ui(self):
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
             self.decimals = 4
             self.decimals = 4
@@ -2220,7 +2249,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             pass
             pass
 
 
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         # make a new name for the new Excellon object (the one with edited content)
         # make a new name for the new Excellon object (the one with edited content)
         self.edited_obj_name = self.exc_obj.options['name']
         self.edited_obj_name = self.exc_obj.options['name']
@@ -2980,7 +3009,8 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         # add a first tool in the Tool Table but only if the Excellon Object is empty
         # add a first tool in the Tool Table but only if the Excellon Object is empty
         if not self.tool2tooldia:
         if not self.tool2tooldia:
-            self.on_tool_add(tooldia=float('%.2f' % float(self.app.defaults['excellon_editor_newdia'])))
+            self.on_tool_add(tooldia=float('%.*f' % (self.decimals,
+                                                     float(self.app.defaults['excellon_editor_newdia']))))
 
 
     def update_fcexcellon(self, exc_obj):
     def update_fcexcellon(self, exc_obj):
         """
         """
@@ -3212,7 +3242,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("There are no Tools definitions in the file. Aborting Excellon creation.")
                                      _("There are no Tools definitions in the file. Aborting Excellon creation.")
                                      )
                                      )
-            except:
+            except Exception:
                 msg = '[ERROR] %s' % \
                 msg = '[ERROR] %s' % \
                       _("An internal error has ocurred. See Shell.\n")
                       _("An internal error has ocurred. See Shell.\n")
                 msg += traceback.format_exc()
                 msg += traceback.format_exc()
@@ -3501,7 +3531,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                         self.app.ui.popMenu.popup(self.app.cursor.pos())
                         self.app.ui.popMenu.popup(self.app.cursor.pos())
 
 
         except Exception as e:
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FlatCAMExcEditor.on_exc_click_release() RMB click --> Error: %s" % str(e))
             raise
             raise
 
 
         # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
         # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
@@ -3519,7 +3549,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                     if self.selected:
                     if self.selected:
                         self.replot()
                         self.replot()
         except Exception as e:
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FlatCAMExcEditor.on_exc_click_release() LMB click --> Error: %s" % str(e))
             raise
             raise
 
 
     def draw_selection_area_handler(self, start, end, sel_type):
     def draw_selection_area_handler(self, start, end, sel_type):

+ 129 - 137
flatcamEditors/FlatCAMGeoEditor.py

@@ -54,6 +54,7 @@ class BufferSelectionTool(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.draw_app = draw_app
         self.draw_app = draw_app
+        self.decimals = app.decimals
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
@@ -80,7 +81,7 @@ class BufferSelectionTool(FlatCAMTool):
 
 
         # Buffer distance
         # Buffer distance
         self.buffer_distance_entry = FCDoubleSpinner()
         self.buffer_distance_entry = FCDoubleSpinner()
-        self.buffer_distance_entry.set_precision(4)
+        self.buffer_distance_entry.set_precision(self.decimals)
         self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
@@ -199,6 +200,7 @@ class TextInputTool(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.text_path = []
         self.text_path = []
+        self.decimals = self.app.decimals
 
 
         self.f_parse = ParseFont(self)
         self.f_parse = ParseFont(self)
         self.f_parse.get_fonts_by_types()
         self.f_parse.get_fonts_by_types()
@@ -367,7 +369,7 @@ class TextInputTool(FlatCAMTool):
                     font_name=self.font_name,
                     font_name=self.font_name,
                     font_size=font_to_geo_size,
                     font_size=font_to_geo_size,
                     font_type=font_to_geo_type,
                     font_type=font_to_geo_type,
-                    units=self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper())
+                    units=self.app.defaults['units'].upper())
 
 
     def font_family(self, font):
     def font_family(self, font):
         self.text_input_entry.selectAll()
         self.text_input_entry.selectAll()
@@ -418,6 +420,7 @@ class PaintOptionsTool(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.fcdraw = fcdraw
         self.fcdraw = fcdraw
+        self.decimals = self.app.decimals
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
@@ -432,6 +435,8 @@ class PaintOptionsTool(FlatCAMTool):
 
 
         grid = QtWidgets.QGridLayout()
         grid = QtWidgets.QGridLayout()
         self.layout.addLayout(grid)
         self.layout.addLayout(grid)
+        grid.setColumnStretch(0, 0)
+        grid.setColumnStretch(1, 1)
 
 
         # Tool dia
         # Tool dia
         ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
@@ -441,7 +446,9 @@ class PaintOptionsTool(FlatCAMTool):
         )
         )
         grid.addWidget(ptdlabel, 0, 0)
         grid.addWidget(ptdlabel, 0, 0)
 
 
-        self.painttooldia_entry = FCEntry()
+        self.painttooldia_entry = FCDoubleSpinner()
+        self.painttooldia_entry.set_range(-9999.9999, 9999.9999)
+        self.painttooldia_entry.set_precision(self.decimals)
         grid.addWidget(self.painttooldia_entry, 0, 1)
         grid.addWidget(self.painttooldia_entry, 0, 1)
 
 
         # Overlap
         # Overlap
@@ -453,13 +460,17 @@ class PaintOptionsTool(FlatCAMTool):
               "Adjust the value starting with lower values\n"
               "Adjust the value starting with lower values\n"
               "and increasing it if areas that should be painted are still \n"
               "and increasing it if areas that should be painted are still \n"
               "not painted.\n"
               "not painted.\n"
-              "Lower values = faster processing, faster execution on PCB.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "due of too many paths.")
               "due of too many paths.")
         )
         )
+        self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
+        self.paintoverlap_entry.set_range(0.0000, 1.0000)
+        self.paintoverlap_entry.set_precision(self.decimals)
+        self.paintoverlap_entry.setWrapping(True)
+        self.paintoverlap_entry.setSingleStep(0.1)
+
         grid.addWidget(ovlabel, 1, 0)
         grid.addWidget(ovlabel, 1, 0)
-        self.paintoverlap_entry = FCEntry()
-        self.paintoverlap_entry.setValidator(QtGui.QDoubleValidator(0.0000, 1.0000, 4))
         grid.addWidget(self.paintoverlap_entry, 1, 1)
         grid.addWidget(self.paintoverlap_entry, 1, 1)
 
 
         # Margin
         # Margin
@@ -469,8 +480,11 @@ class PaintOptionsTool(FlatCAMTool):
              "the edges of the polygon to\n"
              "the edges of the polygon to\n"
              "be painted.")
              "be painted.")
         )
         )
+        self.paintmargin_entry = FCDoubleSpinner()
+        self.paintmargin_entry.set_range(-9999.9999, 9999.9999)
+        self.paintmargin_entry.set_precision(self.decimals)
+
         grid.addWidget(marginlabel, 2, 0)
         grid.addWidget(marginlabel, 2, 0)
-        self.paintmargin_entry = FCEntry()
         grid.addWidget(self.paintmargin_entry, 2, 1)
         grid.addWidget(self.paintmargin_entry, 2, 1)
 
 
         # Method
         # Method
@@ -480,12 +494,13 @@ class PaintOptionsTool(FlatCAMTool):
               "<B>Standard</B>: Fixed step inwards.<BR>"
               "<B>Standard</B>: Fixed step inwards.<BR>"
               "<B>Seed-based</B>: Outwards from seed.")
               "<B>Seed-based</B>: Outwards from seed.")
         )
         )
-        grid.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
         self.paintmethod_combo = RadioSet([
             {"label": _("Standard"), "value": "standard"},
             {"label": _("Standard"), "value": "standard"},
             {"label": _("Seed-based"), "value": "seed"},
             {"label": _("Seed-based"), "value": "seed"},
             {"label": _("Straight lines"), "value": "lines"}
             {"label": _("Straight lines"), "value": "lines"}
         ], orientation='vertical', stretch=False)
         ], orientation='vertical', stretch=False)
+
+        grid.addWidget(methodlabel, 3, 0)
         grid.addWidget(self.paintmethod_combo, 3, 1)
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
 
         # Connect lines
         # Connect lines
@@ -494,8 +509,9 @@ class PaintOptionsTool(FlatCAMTool):
            _("Draw lines between resulting\n"
            _("Draw lines between resulting\n"
              "segments to minimize tool lifts.")
              "segments to minimize tool lifts.")
         )
         )
-        grid.addWidget(pathconnectlabel, 4, 0)
         self.pathconnect_cb = FCCheckBox()
         self.pathconnect_cb = FCCheckBox()
+
+        grid.addWidget(pathconnectlabel, 4, 0)
         grid.addWidget(self.pathconnect_cb, 4, 1)
         grid.addWidget(self.pathconnect_cb, 4, 1)
 
 
         contourlabel = QtWidgets.QLabel(_("Contour:"))
         contourlabel = QtWidgets.QLabel(_("Contour:"))
@@ -503,8 +519,9 @@ class PaintOptionsTool(FlatCAMTool):
             _("Cut around the perimeter of the polygon\n"
             _("Cut around the perimeter of the polygon\n"
               "to trim rough edges.")
               "to trim rough edges.")
         )
         )
-        grid.addWidget(contourlabel, 5, 0)
         self.paintcontour_cb = FCCheckBox()
         self.paintcontour_cb = FCCheckBox()
+
+        grid.addWidget(contourlabel, 5, 0)
         grid.addWidget(self.paintcontour_cb, 5, 1)
         grid.addWidget(self.paintcontour_cb, 5, 1)
 
 
         # Buttons
         # Buttons
@@ -569,40 +586,10 @@ class PaintOptionsTool(FlatCAMTool):
                                  _("Paint cancelled. No shape selected."))
                                  _("Paint cancelled. No shape selected."))
             return
             return
 
 
-        try:
-            tooldia = float(self.painttooldia_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                tooldia = float(self.painttooldia_entry.get_value().replace(',', '.'))
-                self.painttooldia_entry.set_value(tooldia)
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Tool diameter value is missing or wrong format. Add it and retry."))
-                return
-        try:
-            overlap = float(self.paintoverlap_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                overlap = float(self.paintoverlap_entry.get_value().replace(',', '.'))
-                self.paintoverlap_entry.set_value(overlap)
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Overlap value is missing or wrong format. Add it and retry."))
-                return
+        tooldia = float(self.painttooldia_entry.get_value())
+        overlap = float(self.paintoverlap_entry.get_value())
+        margin = float(self.paintmargin_entry.get_value())
 
 
-        try:
-            margin = float(self.paintmargin_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
-                self.paintmargin_entry.set_value(margin)
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Margin distance value is missing or wrong format. Add it and retry."))
-                return
         method = self.paintmethod_combo.get_value()
         method = self.paintmethod_combo.get_value()
         contour = self.paintcontour_cb.get_value()
         contour = self.paintcontour_cb.get_value()
         connect = self.pathconnect_cb.get_value()
         connect = self.pathconnect_cb.get_value()
@@ -632,6 +619,7 @@ class TransformEditorTool(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.draw_app = draw_app
         self.draw_app = draw_app
+        self.decimals = self.app.decimals
 
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
         self.layout.addLayout(self.transform_lay)
@@ -678,9 +666,11 @@ class TransformEditorTool(FlatCAMTool):
         )
         )
         self.rotate_label.setFixedWidth(50)
         self.rotate_label.setFixedWidth(50)
 
 
-        self.rotate_entry = FCEntry()
-        # self.rotate_entry.setFixedWidth(60)
-        self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.rotate_entry = FCDoubleSpinner()
+        self.rotate_entry.set_precision(self.decimals)
+        self.rotate_entry.set_range(-360.0000, 360.0000)
+        self.rotate_entry.setSingleStep(0.1)
+        self.rotate_entry.setWrapping(True)
 
 
         self.rotate_button = FCButton()
         self.rotate_button = FCButton()
         self.rotate_button.set_value(_("Rotate"))
         self.rotate_button.set_value(_("Rotate"))
@@ -714,9 +704,11 @@ class TransformEditorTool(FlatCAMTool):
             "Float number between -360 and 359.")
             "Float number between -360 and 359.")
         )
         )
         self.skewx_label.setFixedWidth(50)
         self.skewx_label.setFixedWidth(50)
-        self.skewx_entry = FCEntry()
-        self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.skewx_entry.setFixedWidth(60)
+        self.skewx_entry = FCDoubleSpinner()
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.set_range(-360.0000, 360.0000)
+        self.skewx_entry.setSingleStep(0.1)
+        self.skewx_entry.setWrapping(True)
 
 
         self.skewx_button = FCButton()
         self.skewx_button = FCButton()
         self.skewx_button.set_value(_("Skew X"))
         self.skewx_button.set_value(_("Skew X"))
@@ -732,9 +724,11 @@ class TransformEditorTool(FlatCAMTool):
              "Float number between -360 and 359.")
              "Float number between -360 and 359.")
         )
         )
         self.skewy_label.setFixedWidth(50)
         self.skewy_label.setFixedWidth(50)
-        self.skewy_entry = FCEntry()
-        self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.skewy_entry.setFixedWidth(60)
+        self.skewy_entry = FCDoubleSpinner()
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.set_range(-360.0000, 360.0000)
+        self.skewy_entry.setSingleStep(0.1)
+        self.skewy_entry.setWrapping(True)
 
 
         self.skewy_button = FCButton()
         self.skewy_button = FCButton()
         self.skewy_button.set_value(_("Skew Y"))
         self.skewy_button.set_value(_("Skew Y"))
@@ -770,9 +764,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Factor for Scale action over X axis.")
             _("Factor for Scale action over X axis.")
         )
         )
         self.scalex_label.setFixedWidth(50)
         self.scalex_label.setFixedWidth(50)
-        self.scalex_entry = FCEntry()
-        self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.scalex_entry.setFixedWidth(60)
+        self.scalex_entry = FCDoubleSpinner()
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.set_range(0.0000, 9999.9999)
+        self.scalex_entry.setSingleStep(0.1)
+        self.scalex_entry.setWrapping(True)
 
 
         self.scalex_button = FCButton()
         self.scalex_button = FCButton()
         self.scalex_button.set_value(_("Scale X"))
         self.scalex_button.set_value(_("Scale X"))
@@ -787,9 +783,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Factor for Scale action over Y axis.")
             _("Factor for Scale action over Y axis.")
         )
         )
         self.scaley_label.setFixedWidth(50)
         self.scaley_label.setFixedWidth(50)
-        self.scaley_entry = FCEntry()
-        self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.scaley_entry.setFixedWidth(60)
+        self.scaley_entry = FCDoubleSpinner()
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.set_range(0.0000, 9999.9999)
+        self.scaley_entry.setSingleStep(0.1)
+        self.scaley_entry.setWrapping(True)
 
 
         self.scaley_button = FCButton()
         self.scaley_button = FCButton()
         self.scaley_button.set_value(_("Scale Y"))
         self.scaley_button.set_value(_("Scale Y"))
@@ -844,9 +842,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Value for Offset action on X axis.")
             _("Value for Offset action on X axis.")
         )
         )
         self.offx_label.setFixedWidth(50)
         self.offx_label.setFixedWidth(50)
-        self.offx_entry = FCEntry()
-        self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.offx_entry.setFixedWidth(60)
+        self.offx_entry = FCDoubleSpinner()
+        self.offx_entry.set_precision(self.decimals)
+        self.offx_entry.set_range(-9999.9999, 9999.9999)
+        self.offx_entry.setSingleStep(0.1)
+        self.offx_entry.setWrapping(True)
 
 
         self.offx_button = FCButton()
         self.offx_button = FCButton()
         self.offx_button.set_value(_("Offset X"))
         self.offx_button.set_value(_("Offset X"))
@@ -862,9 +862,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Value for Offset action on Y axis.")
             _("Value for Offset action on Y axis.")
         )
         )
         self.offy_label.setFixedWidth(50)
         self.offy_label.setFixedWidth(50)
-        self.offy_entry = FCEntry()
-        self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.offy_entry.setFixedWidth(60)
+        self.offy_entry = FCDoubleSpinner()
+        self.offy_entry.set_precision(self.decimals)
+        self.offy_entry.set_range(-9999.9999, 9999.9999)
+        self.offy_entry.setSingleStep(0.1)
+        self.offy_entry.setWrapping(True)
 
 
         self.offy_button = FCButton()
         self.offy_button = FCButton()
         self.offy_button.set_value(_("Offset Y"))
         self.offy_button.set_value(_("Offset Y"))
@@ -903,7 +905,6 @@ class TransformEditorTool(FlatCAMTool):
             _("Flip the selected shape(s) over the X axis.\n"
             _("Flip the selected shape(s) over the X axis.\n"
               "Does not create a new shape.")
               "Does not create a new shape.")
         )
         )
-        self.flipx_button.setFixedWidth(60)
 
 
         self.flipy_button = FCButton()
         self.flipy_button = FCButton()
         self.flipy_button.set_value(_("Flip on Y"))
         self.flipy_button.set_value(_("Flip on Y"))
@@ -911,7 +912,6 @@ class TransformEditorTool(FlatCAMTool):
             _("Flip the selected shape(s) over the X axis.\n"
             _("Flip the selected shape(s) over the X axis.\n"
               "Does not create a new shape.")
               "Does not create a new shape.")
         )
         )
-        self.flipy_button.setFixedWidth(60)
 
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
         self.flip_ref_cb.set_value(True)
@@ -936,9 +936,7 @@ class TransformEditorTool(FlatCAMTool):
               "the 'y' in (x, y) will be used when using Flip on Y.")
               "the 'y' in (x, y) will be used when using Flip on Y.")
         )
         )
         self.flip_ref_label.setFixedWidth(50)
         self.flip_ref_label.setFixedWidth(50)
-        self.flip_ref_entry = EvalEntry2("(0, 0)")
-        self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.flip_ref_entry.setFixedWidth(60)
+        self.flip_ref_entry = FCEntry("(0, 0)")
 
 
         self.flip_ref_button = FCButton()
         self.flip_ref_button = FCButton()
         self.flip_ref_button.set_value(_("Add"))
         self.flip_ref_button.set_value(_("Add"))
@@ -949,7 +947,6 @@ class TransformEditorTool(FlatCAMTool):
            )
            )
         self.flip_ref_button.setFixedWidth(60)
         self.flip_ref_button.setFixedWidth(60)
 
 
-        form4_child_hlay.addStretch()
         form4_child_hlay.addWidget(self.flipx_button)
         form4_child_hlay.addWidget(self.flipx_button)
         form4_child_hlay.addWidget(self.flipy_button)
         form4_child_hlay.addWidget(self.flipy_button)
 
 
@@ -1297,8 +1294,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_("Rotation action was not executed"), str(e)))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Rotation action was not executed"), str(e)))
                     return
                     return
 
 
     def on_flip(self, axis):
     def on_flip(self, axis):
@@ -1358,8 +1354,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_("Flip action was not executed"), str(e)))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Flip action was not executed"), str(e)))
                     return
                     return
 
 
     def on_skew(self, axis, num):
     def on_skew(self, axis, num):
@@ -1405,8 +1400,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_("Skew action was not executed"), str(e)))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Skew action was not executed"), str(e)))
                     return
                     return
 
 
     def on_scale(self, axis, xfactor, yfactor, point=None):
     def on_scale(self, axis, xfactor, yfactor, point=None):
@@ -1462,8 +1456,7 @@ class TransformEditorTool(FlatCAMTool):
                                              _('Scale on the Y axis done'))
                                              _('Scale on the Y axis done'))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_("Scale action was not executed"), str(e)))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Scale action was not executed"), str(e)))
                     return
                     return
 
 
     def on_offset(self, axis, num):
     def on_offset(self, axis, num):
@@ -1496,14 +1489,13 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_("Offset action was not executed"), str(e)))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Offset action was not executed"), str(e)))
                     return
                     return
 
 
     def on_rotate_key(self):
     def on_rotate_key(self):
         val_box = FCInputDialog(title=_("Rotate ..."),
         val_box = FCInputDialog(title=_("Rotate ..."),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
-                                min=-359.9999, max=360.0000, decimals=4,
+                                min=-359.9999, max=360.0000, decimals=self.decimals,
                                 init_val=float(self.app.defaults['tools_transform_rotate']))
                                 init_val=float(self.app.defaults['tools_transform_rotate']))
         val_box.setWindowIcon(QtGui.QIcon('share/rotate.png'))
         val_box.setWindowIcon(QtGui.QIcon('share/rotate.png'))
 
 
@@ -1518,11 +1510,11 @@ class TransformEditorTool(FlatCAMTool):
                                  _("Geometry shape rotate cancelled"))
                                  _("Geometry shape rotate cancelled"))
 
 
     def on_offx_key(self):
     def on_offx_key(self):
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        units = self.app.defaults['units'].lower()
 
 
         val_box = FCInputDialog(title=_("Offset on X axis ..."),
         val_box = FCInputDialog(title=_("Offset on X axis ..."),
                                 text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
                                 text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
-                                min=-9999.9999, max=10000.0000, decimals=4,
+                                min=-9999.9999, max=10000.0000, decimals=self.decimals,
                                 init_val=float(self.app.defaults['tools_transform_offset_x']))
                                 init_val=float(self.app.defaults['tools_transform_offset_x']))
         val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png'))
         val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png'))
 
 
@@ -1537,11 +1529,11 @@ class TransformEditorTool(FlatCAMTool):
                                  _("Geometry shape offset X cancelled"))
                                  _("Geometry shape offset X cancelled"))
 
 
     def on_offy_key(self):
     def on_offy_key(self):
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        units = self.app.defaults['units'].lower()
 
 
         val_box = FCInputDialog(title=_("Offset on Y axis ..."),
         val_box = FCInputDialog(title=_("Offset on Y axis ..."),
                                 text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
                                 text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
-                                min=-9999.9999, max=10000.0000, decimals=4,
+                                min=-9999.9999, max=10000.0000, decimals=self.decimals,
                                 init_val=float(self.app.defaults['tools_transform_offset_y']))
                                 init_val=float(self.app.defaults['tools_transform_offset_y']))
         val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png'))
         val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png'))
 
 
@@ -1558,7 +1550,7 @@ class TransformEditorTool(FlatCAMTool):
     def on_skewx_key(self):
     def on_skewx_key(self):
         val_box = FCInputDialog(title=_("Skew on X axis ..."),
         val_box = FCInputDialog(title=_("Skew on X axis ..."),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
-                                min=-359.9999, max=360.0000, decimals=4,
+                                min=-359.9999, max=360.0000, decimals=self.decimals,
                                 init_val=float(self.app.defaults['tools_transform_skew_x']))
                                 init_val=float(self.app.defaults['tools_transform_skew_x']))
         val_box.setWindowIcon(QtGui.QIcon('share/skewX.png'))
         val_box.setWindowIcon(QtGui.QIcon('share/skewX.png'))
 
 
@@ -1575,7 +1567,7 @@ class TransformEditorTool(FlatCAMTool):
     def on_skewy_key(self):
     def on_skewy_key(self):
         val_box = FCInputDialog(title=_("Skew on Y axis ..."),
         val_box = FCInputDialog(title=_("Skew on Y axis ..."),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
                                 text='%s:' % _('Enter an Angle Value (degrees)'),
-                                min=-359.9999, max=360.0000, decimals=4,
+                                min=-359.9999, max=360.0000, decimals=self.decimals,
                                 init_val=float(self.app.defaults['tools_transform_skew_y']))
                                 init_val=float(self.app.defaults['tools_transform_skew_y']))
         val_box.setWindowIcon(QtGui.QIcon('share/skewY.png'))
         val_box.setWindowIcon(QtGui.QIcon('share/skewY.png'))
 
 
@@ -1815,7 +1807,7 @@ class DrawToolShape(object):
 
 
         try:
         try:
             xfactor = float(xfactor)
             xfactor = float(xfactor)
-        except:
+        except Exception:
             log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
             log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
             return
             return
 
 
@@ -1824,7 +1816,7 @@ class DrawToolShape(object):
         else:
         else:
             try:
             try:
                 yfactor = float(yfactor)
                 yfactor = float(yfactor)
-            except:
+            except Exception:
                 log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
                 log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
                 return
                 return
 
 
@@ -1946,7 +1938,7 @@ class FCCircle(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -1979,7 +1971,7 @@ class FCCircle(FCShapeTool):
     def make(self):
     def make(self):
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
 
 
         p1 = self.points[0]
         p1 = self.points[0]
@@ -1998,7 +1990,7 @@ class FCArc(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2217,7 +2209,7 @@ class FCRectangle(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2248,7 +2240,7 @@ class FCRectangle(FCShapeTool):
     def make(self):
     def make(self):
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
 
 
         p1 = self.points[0]
         p1 = self.points[0]
@@ -2271,7 +2263,7 @@ class FCPolygon(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2304,7 +2296,7 @@ class FCPolygon(FCShapeTool):
     def make(self):
     def make(self):
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
 
 
         # self.geometry = LinearRing(self.points)
         # self.geometry = LinearRing(self.points)
@@ -2334,7 +2326,7 @@ class FCPath(FCPolygon):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_path5.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_path5.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2694,7 +2686,7 @@ class FCText(FCShapeTool):
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_text.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_text.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2748,7 +2740,7 @@ class FCText(FCShapeTool):
 
 
         try:
         try:
             return DrawToolUtilityShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
             return DrawToolUtilityShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
-        except:
+        except Exception:
             return
             return
 
 
 
 
@@ -3033,6 +3025,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
+        self.decimals = app.decimals
 
 
         # ## Toolbar events and properties
         # ## Toolbar events and properties
         self.tools = {
         self.tools = {
@@ -3139,7 +3132,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
         }
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
 
         for option in self.options:
         for option in self.options:
             if option in self.app.options:
             if option in self.app.options:
@@ -3152,9 +3145,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.rtree_index = rtindex.Index()
         self.rtree_index = rtindex.Index()
 
 
-        # Number of decimals used by tools in this class
-        self.decimals = 4
-
         def entry2option(option, entry):
         def entry2option(option, entry):
             try:
             try:
                 self.options[option] = float(entry.text())
                 self.options[option] = float(entry.text())
@@ -3170,10 +3160,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
             except ValueError:
             except ValueError:
                 return
                 return
 
 
-            units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-            dec = 6 if units == 'IN' else 4
+            units = self.app.defaults['units'].upper()
+
             if self.app.ui.grid_gap_link_cb.isChecked():
             if self.app.ui.grid_gap_link_cb.isChecked():
-                self.app.ui.grid_gap_y_entry.set_value(val, decimals=dec)
+                self.app.ui.grid_gap_y_entry.set_value(val, decimals=self.decimals)
 
 
         self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.textChanged.connect(
         self.app.ui.grid_gap_x_entry.textChanged.connect(
@@ -3649,12 +3639,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.replot()
         self.replot()
 
 
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        if self.units == "IN":
-            self.decimals = 4
-        else:
-            self.decimals = 2
+        self.units = self.app.defaults['units'].upper()
+        self.decimals = self.app.decimals
 
 
         # start with GRID toolbar activated
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
@@ -3916,7 +3902,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                                      _("Done."))
                                                      _("Done."))
                                 self.select_tool(self.active_tool.name)
                                 self.select_tool(self.active_tool.name)
         except Exception as e:
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FLatCAMGeoEditor.on_geo_click_release() --> Error: %s" % str(e))
             return
             return
 
 
     def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
     def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
@@ -4246,7 +4232,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # # ## Grid snap
         # # ## Grid snap
         if self.options["grid_snap"]:
         if self.options["grid_snap"]:
             if self.options["global_gridx"] != 0:
             if self.options["global_gridx"] != 0:
-                snap_x_ = round(x / float(self.options["global_gridx"])) * float(self.options['global_gridx'])
+                try:
+                    snap_x_ = round(x / float(self.options["global_gridx"])) * float(self.options['global_gridx'])
+                except TypeError:
+                    snap_x_ = x
             else:
             else:
                 snap_x_ = x
                 snap_x_ = x
 
 
@@ -4254,12 +4243,18 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # and it will use the snap distance from GridX entry
             # and it will use the snap distance from GridX entry
             if self.app.ui.grid_gap_link_cb.isChecked():
             if self.app.ui.grid_gap_link_cb.isChecked():
                 if self.options["global_gridx"] != 0:
                 if self.options["global_gridx"] != 0:
-                    snap_y_ = round(y / float(self.options["global_gridx"])) * float(self.options['global_gridx'])
+                    try:
+                        snap_y_ = round(y / float(self.options["global_gridx"])) * float(self.options['global_gridx'])
+                    except TypeError:
+                        snap_y_ = y
                 else:
                 else:
                     snap_y_ = y
                     snap_y_ = y
             else:
             else:
                 if self.options["global_gridy"] != 0:
                 if self.options["global_gridy"] != 0:
-                    snap_y_ = round(y / float(self.options["global_gridy"])) * float(self.options['global_gridy'])
+                    try:
+                        snap_y_ = round(y / float(self.options["global_gridy"])) * float(self.options['global_gridy'])
+                    except TypeError:
+                        snap_y_ = y
                 else:
                 else:
                     snap_y_ = y
                     snap_y_ = y
             nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
             nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
@@ -4282,11 +4277,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
             for shape in self.storage.get_objects():
             for shape in self.storage.get_objects():
                 fcgeometry.tools[self.multigeo_tool]['solid_geometry'].append(shape.geo)
                 fcgeometry.tools[self.multigeo_tool]['solid_geometry'].append(shape.geo)
             self.multigeo_tool = None
             self.multigeo_tool = None
-        else:
-            fcgeometry.solid_geometry = []
-            # for shape in self.shape_buffer:
-            for shape in self.storage.get_objects():
-                fcgeometry.solid_geometry.append(shape.geo)
+
+        fcgeometry.solid_geometry = []
+        # for shape in self.shape_buffer:
+        for shape in self.storage.get_objects():
+            fcgeometry.solid_geometry.append(shape.geo)
 
 
     def update_options(self, obj):
     def update_options(self, obj):
         if self.paint_tooldia:
         if self.paint_tooldia:
@@ -4605,27 +4600,25 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
     def paint(self, tooldia, overlap, margin, connect, contour, method):
     def paint(self, tooldia, overlap, margin, connect, contour, method):
 
 
+        if overlap >= 1:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Could not do Paint. Overlap value has to be less than 1.00 (100%%)."))
+            return
+
         self.paint_tooldia = tooldia
         self.paint_tooldia = tooldia
         selected = self.get_selected()
         selected = self.get_selected()
 
 
         if len(selected) == 0:
         if len(selected) == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Nothing selected for painting."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected for painting."))
             return
             return
 
 
         for param in [tooldia, overlap, margin]:
         for param in [tooldia, overlap, margin]:
             if not isinstance(param, float):
             if not isinstance(param, float):
                 param_name = [k for k, v in locals().items() if v is param][0]
                 param_name = [k for k, v in locals().items() if v is param][0]
-                self.app.inform.emit('[WARNING] %s: %s' %
-                                     (_("Invalid value for"), str(param)))
+                self.app.inform.emit('[WARNING] %s: %s' % (_("Invalid value for"), str(param)))
 
 
         results = []
         results = []
 
 
-        if overlap >= 1:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Could not do Paint. Overlap value has to be less than 1.00 (100%%)."))
-            return
-
         def recurse(geometry, reset=True):
         def recurse(geometry, reset=True):
             """
             """
             Creates a list of non-iterable linear geometry objects.
             Creates a list of non-iterable linear geometry objects.
@@ -4664,17 +4657,16 @@ class FlatCAMGeoEditor(QtCore.QObject):
                         poly_buf = Polygon(geo_obj).buffer(-margin)
                         poly_buf = Polygon(geo_obj).buffer(-margin)
 
 
                     if method == "seed":
                     if method == "seed":
-                        cp = Geometry.clear_polygon2(poly_buf,
-                                                     tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon2(self, polygon_to_clear=poly_buf, tooldia=tooldia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                      overlap=overlap, contour=contour, connect=connect)
                                                      overlap=overlap, contour=contour, connect=connect)
                     elif method == "lines":
                     elif method == "lines":
-                        cp = Geometry.clear_polygon3(poly_buf,
-                                                     tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon3(self, polygon=poly_buf, tooldia=tooldia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                      overlap=overlap, contour=contour, connect=connect)
                                                      overlap=overlap, contour=contour, connect=connect)
-
                     else:
                     else:
-                        cp = Geometry.clear_polygon(poly_buf,
-                                                    tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon(self, polygon=poly_buf, tooldia=tooldia,
+                                                    steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                     overlap=overlap, contour=contour, connect=connect)
                                                     overlap=overlap, contour=contour, connect=connect)
 
 
                     if cp is not None:
                     if cp is not None:

Plik diff jest za duży
+ 325 - 311
flatcamEditors/FlatCAMGrbEditor.py


+ 29 - 14
flatcamEditors/FlatCAMTextEditor.py

@@ -19,7 +19,7 @@ if '_' not in builtins.__dict__:
 
 
 class TextEditor(QtWidgets.QWidget):
 class TextEditor(QtWidgets.QWidget):
 
 
-    def __init__(self, app, text=None):
+    def __init__(self, app, text=None, plain_text=None):
         super().__init__()
         super().__init__()
 
 
         self.app = app
         self.app = app
@@ -39,12 +39,24 @@ class TextEditor(QtWidgets.QWidget):
         self.work_editor_layout.setContentsMargins(2, 2, 2, 2)
         self.work_editor_layout.setContentsMargins(2, 2, 2, 2)
         self.t_frame.setLayout(self.work_editor_layout)
         self.t_frame.setLayout(self.work_editor_layout)
 
 
-        self.code_editor = FCTextAreaExtended()
-        stylesheet = """
-                        QTextEdit { selection-background-color:yellow;
-                                    selection-color:black;
-                        }
-                     """
+        if plain_text:
+            self.editor_class = FCTextAreaLineNumber()
+            self.code_editor = self.editor_class.edit
+
+            stylesheet = """
+                            QPlainTextEdit { selection-background-color:yellow;
+                                             selection-color:black;
+                            }
+                         """
+            self.work_editor_layout.addWidget(self.editor_class, 0, 0, 1, 5)
+        else:
+            self.code_editor = FCTextAreaExtended()
+            stylesheet = """
+                            QTextEdit { selection-background-color:yellow;
+                                        selection-color:black;
+                            }
+                         """
+            self.work_editor_layout.addWidget(self.code_editor, 0, 0, 1, 5)
 
 
         self.code_editor.setStyleSheet(stylesheet)
         self.code_editor.setStyleSheet(stylesheet)
 
 
@@ -90,7 +102,6 @@ class TextEditor(QtWidgets.QWidget):
         self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one."))
         self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one."))
 
 
         self.buttonRun.hide()
         self.buttonRun.hide()
-        self.work_editor_layout.addWidget(self.code_editor, 0, 0, 1, 5)
 
 
         editor_hlay_1 = QtWidgets.QHBoxLayout()
         editor_hlay_1 = QtWidgets.QHBoxLayout()
         # cnc_tab_lay_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         # cnc_tab_lay_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
@@ -130,7 +141,7 @@ class TextEditor(QtWidgets.QWidget):
 
 
         self.code_editor.set_model_data(self.app.myKeywords)
         self.code_editor.set_model_data(self.app.myKeywords)
 
 
-        self.gcode_edited = ''
+        self.code_edited = ''
 
 
     def handlePrint(self):
     def handlePrint(self):
         self.app.report_usage("handlePrint()")
         self.app.report_usage("handlePrint()")
@@ -168,11 +179,11 @@ class TextEditor(QtWidgets.QWidget):
             file = QtCore.QFile(path)
             file = QtCore.QFile(path)
             if file.open(QtCore.QIODevice.ReadOnly):
             if file.open(QtCore.QIODevice.ReadOnly):
                 stream = QtCore.QTextStream(file)
                 stream = QtCore.QTextStream(file)
-                self.gcode_edited = stream.readAll()
-                self.code_editor.setPlainText(self.gcode_edited)
+                self.code_edited = stream.readAll()
+                self.code_editor.setPlainText(self.code_edited)
                 file.close()
                 file.close()
 
 
-    def handleSaveGCode(self, name=None, filt=None):
+    def handleSaveGCode(self, name=None, filt=None, callback=None):
         self.app.report_usage("handleSaveGCode()")
         self.app.report_usage("handleSaveGCode()")
 
 
         if filt:
         if filt:
@@ -193,12 +204,12 @@ class TextEditor(QtWidgets.QWidget):
 
 
         try:
         try:
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export G-Code ..."),
+                caption=_("Export Code ..."),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
                 filter=_filter_
                 filter=_filter_
             )[0])
             )[0])
         except TypeError:
         except TypeError:
-            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export G-Code ..."), filter=_filter_)[0])
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Code ..."), filter=_filter_)[0])
 
 
         if filename == "":
         if filename == "":
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Code cancelled."))
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Code cancelled."))
@@ -224,6 +235,9 @@ class TextEditor(QtWidgets.QWidget):
         self.app.file_saved.emit("cncjob", filename)
         self.app.file_saved.emit("cncjob", filename)
         self.app.inform.emit('%s: %s' % (_("Saved to"), str(filename)))
         self.app.inform.emit('%s: %s' % (_("Saved to"), str(filename)))
 
 
+        if callback is not None:
+            callback()
+
     def handleFindGCode(self):
     def handleFindGCode(self):
         self.app.report_usage("handleFindGCode()")
         self.app.report_usage("handleFindGCode()")
 
 
@@ -233,6 +247,7 @@ class TextEditor(QtWidgets.QWidget):
         r = self.code_editor.find(str(text_to_be_found), flags)
         r = self.code_editor.find(str(text_to_be_found), flags)
         if r is False:
         if r is False:
             self.code_editor.moveCursor(QtGui.QTextCursor.Start)
             self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+            r = self.code_editor.find(str(text_to_be_found), flags)
 
 
     def handleReplaceGCode(self):
     def handleReplaceGCode(self):
         self.app.report_usage("handleReplaceGCode()")
         self.app.report_usage("handleReplaceGCode()")

+ 223 - 443
flatcamGUI/FlatCAMGUI.py

@@ -32,10 +32,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
     geom_update = QtCore.pyqtSignal(int, int, int, int, int, name='geomUpdate')
     geom_update = QtCore.pyqtSignal(int, int, int, int, int, name='geomUpdate')
     final_save = QtCore.pyqtSignal(name='saveBeforeExit')
     final_save = QtCore.pyqtSignal(name='saveBeforeExit')
 
 
-    def __init__(self, version, beta, app):
+    def __init__(self, app):
         super(FlatCAMGUI, self).__init__()
         super(FlatCAMGUI, self).__init__()
 
 
         self.app = app
         self.app = app
+        self.decimals = self.app.decimals
+
         # Divine icon pack by Ipapun @ finicons.com
         # Divine icon pack by Ipapun @ finicons.com
 
 
         # ################################## ##
         # ################################## ##
@@ -337,21 +339,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.menuedit.addSeparator()
         self.menuedit.addSeparator()
         self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), _('&Preferences\tSHIFT+P'))
         self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), _('&Preferences\tSHIFT+P'))
 
 
-        # ## Options # ##
-        self.menuoptions = self.menu.addMenu(_('Options'))
-        # self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options')
-        # self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project")
-        # self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application")
-        # self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object")
-        # self.menuoptions_transfer_o2p = self.menuoptions_transfer.addAction("Object to Project")
-        # self.menuoptions_transfer_a2o = self.menuoptions_transfer.addAction("Application to Object")
-        # self.menuoptions_transfer_o2a = self.menuoptions_transfer.addAction("Object to Application")
-
-        # Separator
-        # self.menuoptions.addSeparator()
+        # ########################################################################
+        # ########################## OPTIONS # ###################################
+        # ########################################################################
 
 
-        # self.menuoptions_transform = self.menuoptions.addMenu(QtGui.QIcon('share/transform.png'),
-        #                                                       '&Transform Object')
+        self.menuoptions = self.menu.addMenu(_('Options'))
         self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'),
         self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'),
                                                                        _("&Rotate Selection\tSHIFT+(R)"))
                                                                        _("&Rotate Selection\tSHIFT+(R)"))
         # Separator
         # Separator
@@ -373,13 +365,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'),
         self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'),
                                                                   _("View source\tALT+S"))
                                                                   _("View source\tALT+S"))
+        self.menuoptions_tools_db = self.menuoptions.addAction(QtGui.QIcon('share/database32.png'),
+                                                               _("Tools DataBase\tCTRL+D"))
         # Separator
         # Separator
         self.menuoptions.addSeparator()
         self.menuoptions.addSeparator()
 
 
         # ########################################################################
         # ########################################################################
         # ########################## View # ######################################
         # ########################## View # ######################################
         # ########################################################################
         # ########################################################################
-        self.menuview = self.menu.addMenu(_('&View'))
+        self.menuview = self.menu.addMenu(_('View'))
         self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), _('Enable all plots\tALT+1'))
         self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), _('Enable all plots\tALT+1'))
         self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
         self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
                                                           _('Disable all plots\tALT+2'))
                                                           _('Disable all plots\tALT+2'))
@@ -430,7 +424,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################################################################
         # ########################## Tool # ######################################
         # ########################## Tool # ######################################
         # ########################################################################
         # ########################################################################
-        self.menutool = QtWidgets.QMenu(_('&Tool'))
+        self.menutool = QtWidgets.QMenu(_('Tool'))
         self.menutoolaction = self.menu.addMenu(self.menutool)
         self.menutoolaction = self.menu.addMenu(self.menutool)
         self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), _('&Command Line\tS'))
         self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), _('&Command Line\tS'))
 
 
@@ -755,6 +749,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'), _("Calculators Tool"))
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'), _("Calculators Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool"))
+        self.qrcode_btn = self.toolbartools.addAction(QtGui.QIcon('share/qrcode32.png'), _("QRCode Tool"))
+        self.copperfill_btn = self.toolbartools.addAction(QtGui.QIcon('share/copperfill32.png'),
+                                                          _("Copper Thieving Tool"))
+
+        self.fiducials_btn = self.toolbartools.addAction(QtGui.QIcon('share/fiducials_32.png'), _("Fiducials Tool"))
 
 
         # ########################################################################
         # ########################################################################
         # ########################## Excellon Editor Toolbar# ####################
         # ########################## Excellon Editor Toolbar# ####################
@@ -956,6 +955,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################## PREFERENCES AREA Tab # ######################
         # ########################## PREFERENCES AREA Tab # ######################
         # ########################################################################
         # ########################################################################
         self.preferences_tab = QtWidgets.QWidget()
         self.preferences_tab = QtWidgets.QWidget()
+        self.preferences_tab.setObjectName("preferences_tab")
         self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
         self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
         self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
         self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
 
 
@@ -976,13 +976,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.hlay1 = QtWidgets.QHBoxLayout()
         self.hlay1 = QtWidgets.QHBoxLayout()
         self.general_tab_lay.addLayout(self.hlay1)
         self.general_tab_lay.addLayout(self.hlay1)
 
 
-        self.options_combo = QtWidgets.QComboBox()
-        self.options_combo.addItem(_("APP.  DEFAULTS"))
-        self.options_combo.addItem(_("PROJ. OPTIONS "))
-        self.hlay1.addWidget(self.options_combo)
-
-        # disable this button as it may no longer be useful
-        self.options_combo.setVisible(False)
         self.hlay1.addStretch()
         self.hlay1.addStretch()
 
 
         self.general_scroll_area = QtWidgets.QScrollArea()
         self.general_scroll_area = QtWidgets.QScrollArea()
@@ -1093,14 +1086,28 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.pref_tab_bottom_layout_2.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pref_tab_bottom_layout_2.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_2)
         self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_2)
 
 
+        self.pref_apply_button = FCButton()
+        self.pref_apply_button.setText(_("Apply"))
+        self.pref_apply_button.setMinimumWidth(130)
+        self.pref_apply_button.setToolTip(
+            _("Apply the current preferences without saving to a file."))
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_apply_button)
+
         self.pref_save_button = QtWidgets.QPushButton()
         self.pref_save_button = QtWidgets.QPushButton()
-        self.pref_save_button.setText(_("Save Preferences"))
+        self.pref_save_button.setText(_("Save"))
         self.pref_save_button.setMinimumWidth(130)
         self.pref_save_button.setMinimumWidth(130)
         self.pref_save_button.setToolTip(
         self.pref_save_button.setToolTip(
             _("Save the current settings in the 'current_defaults' file\n"
             _("Save the current settings in the 'current_defaults' file\n"
               "which is the file storing the working default preferences."))
               "which is the file storing the working default preferences."))
         self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button)
         self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button)
 
 
+        self.pref_close_button = QtWidgets.QPushButton()
+        self.pref_close_button.setText(_("Cancel"))
+        self.pref_close_button.setMinimumWidth(130)
+        self.pref_close_button.setToolTip(
+            _("Will not save the changes and will close the preferences window."))
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_close_button)
+
         # ########################################################################
         # ########################################################################
         # #################### SHORTCUT LIST AREA Tab # ##########################
         # #################### SHORTCUT LIST AREA Tab # ##########################
         # ########################################################################
         # ########################################################################
@@ -1234,6 +1241,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>CTRL+C</strong></td>
                         <td height="20"><strong>CTRL+C</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+D</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>CTRL+E</strong></td>
                         <td height="20"><strong>CTRL+E</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1254,6 +1265,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>CTRL+O</strong></td>
                         <td height="20"><strong>CTRL+O</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+Q</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>CTRL+S</strong></td>
                         <td height="20"><strong>CTRL+S</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1322,6 +1337,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>ALT+E</strong></td>
                         <td height="20"><strong>ALT+E</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+J</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>ALT+K</strong></td>
                         <td height="20"><strong>ALT+K</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1428,18 +1447,32 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 _("New Excellon"), _("Move Obj"), _("New Geometry"), _("Set Origin"), _("Change Units"),
                 _("New Excellon"), _("Move Obj"), _("New Geometry"), _("Set Origin"), _("Change Units"),
                 _("Open Properties Tool"), _("Rotate by 90 degree CW"), _("Shell Toggle"),
                 _("Open Properties Tool"), _("Rotate by 90 degree CW"), _("Shell Toggle"),
                 _("Add a Tool (when in Geometry Selected Tab or in Tools NCC or Tools Paint)"), _("Zoom Fit"),
                 _("Add a Tool (when in Geometry Selected Tab or in Tools NCC or Tools Paint)"), _("Zoom Fit"),
-                _("Flip on X_axis"), _("Flip on Y_axis"), _("Zoom Out"), _("Zoom In"), _("Select All"), _("Copy Obj"),
+                _("Flip on X_axis"), _("Flip on Y_axis"), _("Zoom Out"), _("Zoom In"),
+
+                # CTRL section
+                _("Select All"), _("Copy Obj"), _("Open Tools Database"),
                 _("Open Excellon File"), _("Open Gerber File"), _("New Project"), _("Distance Tool"),
                 _("Open Excellon File"), _("Open Gerber File"), _("New Project"), _("Distance Tool"),
-                _("Open Project"), _("Save Project As"), _("Toggle Plot Area"), _("Copy Obj_Name"),
+                _("Open Project"), _("PDF Import Tool"), _("Save Project As"), _("Toggle Plot Area"),
+
+                # SHIFT section
+                _("Copy Obj_Name"),
                 _("Toggle Code Editor"), _("Toggle the axis"), _("Distance Minimum Tool"), _("Open Preferences Window"),
                 _("Toggle Code Editor"), _("Toggle the axis"), _("Distance Minimum Tool"), _("Open Preferences Window"),
                 _("Rotate by 90 degree CCW"), _("Run a Script"), _("Toggle the workspace"), _("Skew on X axis"),
                 _("Rotate by 90 degree CCW"), _("Run a Script"), _("Toggle the workspace"), _("Skew on X axis"),
-                _("Skew on Y axis"), _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"),
+                _("Skew on Y axis"),
+                # ALT section
+                _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"), _("Fiducials Tool"),
                 _("Solder Paste Dispensing Tool"),
                 _("Solder Paste Dispensing Tool"),
                 _("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal Tool"),
                 _("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal Tool"),
-                _("Paint Area Tool"), _("PDF Import Tool"), _("Rules Check Tool"),
+                _("Paint Area Tool"), _("QRCode Tool"), _("Rules Check Tool"),
                 _("View File Source"),
                 _("View File Source"),
                 _("Cutout PCB Tool"), _("Enable all Plots"), _("Disable all Plots"), _("Disable Non-selected Plots"),
                 _("Cutout PCB Tool"), _("Enable all Plots"), _("Disable all Plots"), _("Disable Non-selected Plots"),
-                _("Toggle Full Screen"), _("Abort current task (gracefully)"), _("Open Online Manual"),
+                _("Toggle Full Screen"),
+
+                # CTRL + ALT section
+                _("Abort current task (gracefully)"),
+
+                # F keys section
+                _("Open Online Manual"),
                 _("Open Online Tutorials"), _("Refresh Plots"), _("Delete Object"), _("Alternate: Delete Tool"),
                 _("Open Online Tutorials"), _("Refresh Plots"), _("Delete Object"), _("Alternate: Delete Tool"),
                 _("(left to Key_1)Toogle Notebook Area (Left Side)"), _("En(Dis)able Obj Plot"),
                 _("(left to Key_1)Toogle Notebook Area (Left Side)"), _("En(Dis)able Obj Plot"),
                 _("Deselects all objects")
                 _("Deselects all objects")
@@ -1971,8 +2004,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         self.setGeometry(100, 100, 1024, 650)
         self.setGeometry(100, 100, 1024, 650)
         self.setWindowTitle('FlatCAM %s %s - %s' %
         self.setWindowTitle('FlatCAM %s %s - %s' %
-                            (version,
-                             ('BETA' if beta else ''),
+                            (self.app.version,
+                             ('BETA' if self.app.beta else ''),
                              platform.architecture()[0])
                              platform.architecture()[0])
                             )
                             )
 
 
@@ -1993,23 +2026,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grb_editor_cmenu.menuAction().setVisible(False)
         self.grb_editor_cmenu.menuAction().setVisible(False)
         self.e_editor_cmenu.menuAction().setVisible(False)
         self.e_editor_cmenu.menuAction().setVisible(False)
 
 
-        self.general_defaults_form = GeneralPreferencesUI()
-        self.gerber_defaults_form = GerberPreferencesUI()
-        self.excellon_defaults_form = ExcellonPreferencesUI()
-        self.geometry_defaults_form = GeometryPreferencesUI()
-        self.cncjob_defaults_form = CNCJobPreferencesUI()
-        self.tools_defaults_form = ToolsPreferencesUI()
-        self.tools2_defaults_form = Tools2PreferencesUI()
-        self.util_defaults_form = UtilPreferencesUI()
-
-        self.general_options_form = GeneralPreferencesUI()
-        self.gerber_options_form = GerberPreferencesUI()
-        self.excellon_options_form = ExcellonPreferencesUI()
-        self.geometry_options_form = GeometryPreferencesUI()
-        self.cncjob_options_form = CNCJobPreferencesUI()
-        self.tools_options_form = ToolsPreferencesUI()
-        self.tools2_options_form = Tools2PreferencesUI()
-        self.util_options_form = UtilPreferencesUI()
+        self.general_defaults_form = GeneralPreferencesUI(decimals=self.decimals)
+        self.gerber_defaults_form = GerberPreferencesUI(decimals=self.decimals)
+        self.excellon_defaults_form = ExcellonPreferencesUI(decimals=self.decimals)
+        self.geometry_defaults_form = GeometryPreferencesUI(decimals=self.decimals)
+        self.cncjob_defaults_form = CNCJobPreferencesUI(decimals=self.decimals)
+        self.tools_defaults_form = ToolsPreferencesUI(decimals=self.decimals)
+        self.tools2_defaults_form = Tools2PreferencesUI(decimals=self.decimals)
+        self.util_defaults_form = UtilPreferencesUI(decimals=self.decimals)
 
 
         QtWidgets.qApp.installEventFilter(self)
         QtWidgets.qApp.installEventFilter(self)
 
 
@@ -2020,7 +2044,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             self.restoreState(saved_gui_state)
             self.restoreState(saved_gui_state)
             log.debug("FlatCAMGUI.__init__() --> UI state restored.")
             log.debug("FlatCAMGUI.__init__() --> UI state restored.")
 
 
-        settings = QSettings("Open Source", "FlatCAM")
         if settings.contains("layout"):
         if settings.contains("layout"):
             layout = settings.value('layout', type=str)
             layout = settings.value('layout', type=str)
             if layout == 'standard':
             if layout == 'standard':
@@ -2064,7 +2087,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.lock_action.setText(_("Lock Toolbars"))
         self.lock_action.setText(_("Lock Toolbars"))
         self.lock_action.setCheckable(True)
         self.lock_action.setCheckable(True)
 
 
-        settings = QSettings("Open Source", "FlatCAM")
         if settings.contains("toolbar_lock"):
         if settings.contains("toolbar_lock"):
             lock_val = settings.value('toolbar_lock')
             lock_val = settings.value('toolbar_lock')
             if lock_val == 'true':
             if lock_val == 'true':
@@ -2171,6 +2193,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'),
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'),
                                                            _("Calculators Tool"))
                                                            _("Calculators Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool"))
+        self.qrcode_btn = self.toolbartools.addAction(QtGui.QIcon('share/qrcode32.png'), _("QRCode Tool"))
+        self.copperfill_btn = self.toolbartools.addAction(QtGui.QIcon('share/copperfill32.png'),
+                                                          _("Copper Thieving Tool"))
+
+        self.fiducials_btn = self.toolbartools.addAction(QtGui.QIcon('share/fiducials_32.png'), _("Fiducials Tool"))
 
 
         # ## Excellon Editor Toolbar # ##
         # ## Excellon Editor Toolbar # ##
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
@@ -2369,28 +2396,62 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     return
                     return
 
 
             elif modifiers == QtCore.Qt.ControlModifier:
             elif modifiers == QtCore.Qt.ControlModifier:
+                # Select All
                 if key == QtCore.Qt.Key_A:
                 if key == QtCore.Qt.Key_A:
                     self.app.on_selectall()
                     self.app.on_selectall()
 
 
+                # Copy an FlatCAM object
                 if key == QtCore.Qt.Key_C:
                 if key == QtCore.Qt.Key_C:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_copy()
+                        return
+
                     self.app.on_copy_object()
                     self.app.on_copy_object()
 
 
+                # Copy an FlatCAM object
+                if key == QtCore.Qt.Key_D:
+                    self.app.on_tools_database()
+
+                # Open Excellon file
                 if key == QtCore.Qt.Key_E:
                 if key == QtCore.Qt.Key_E:
                     self.app.on_fileopenexcellon()
                     self.app.on_fileopenexcellon()
 
 
+                # Open Gerber file
                 if key == QtCore.Qt.Key_G:
                 if key == QtCore.Qt.Key_G:
                     self.app.on_fileopengerber()
                     self.app.on_fileopengerber()
 
 
+                # Create New Project
                 if key == QtCore.Qt.Key_N:
                 if key == QtCore.Qt.Key_N:
                     self.app.on_file_new_click()
                     self.app.on_file_new_click()
 
 
+                # Distance Tool
                 if key == QtCore.Qt.Key_M:
                 if key == QtCore.Qt.Key_M:
                     self.app.distance_tool.run()
                     self.app.distance_tool.run()
 
 
+                # Open Project
                 if key == QtCore.Qt.Key_O:
                 if key == QtCore.Qt.Key_O:
                     self.app.on_file_openproject()
                     self.app.on_file_openproject()
 
 
+                # PDF Import
+                if key == QtCore.Qt.Key_Q:
+                    self.app.pdf_tool.run()
+
+                # Save Project
                 if key == QtCore.Qt.Key_S:
                 if key == QtCore.Qt.Key_S:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'preferences_tab':
+                        self.app.on_save_button(save_to_file=False)
+                        return
+
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = False
+                        self.app.tools_db_tab.on_save_tools_db()
+                        return
+
                     self.app.on_file_saveproject()
                     self.app.on_file_saveproject()
 
 
                 # Toggle Plot Area
                 # Toggle Plot Area
@@ -2434,7 +2495,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # Toggle Workspace
                 # Toggle Workspace
                 if key == QtCore.Qt.Key_W:
                 if key == QtCore.Qt.Key_W:
-                    self.app.on_workspace_menu()
+                    self.app.on_workspace_toggle()
                     return
                     return
 
 
                 # Skew on X axis
                 # Skew on X axis
@@ -2469,9 +2530,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.dblsidedtool.run(toggle=True)
                     self.app.dblsidedtool.run(toggle=True)
                     return
                     return
 
 
-                # Transformation Tool
+                # Calibrate  Tool
                 if key == QtCore.Qt.Key_E:
                 if key == QtCore.Qt.Key_E:
-                    self.app.transform_tool.run(toggle=True)
+                    self.app.cal_exc_tool.run(toggle=True)
+                    return
+
+                # Copper Thieving Tool
+                if key == QtCore.Qt.Key_F:
+                    self.app.copper_thieving_tool.run(toggle=True)
                     return
                     return
 
 
                 # Toggle Grid lines
                 # Toggle Grid lines
@@ -2479,6 +2545,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_grid_lines()
                     self.app.on_toggle_grid_lines()
                     return
                     return
 
 
+                # Fiducials Tool
+                if key == QtCore.Qt.Key_J:
+                    self.app.fiducial_tool.run(toggle=True)
+                    return
+
                 # Solder Paste Dispensing Tool
                 # Solder Paste Dispensing Tool
                 if key == QtCore.Qt.Key_K:
                 if key == QtCore.Qt.Key_K:
                     self.app.paste_tool.run(toggle=True)
                     self.app.paste_tool.run(toggle=True)
@@ -2504,9 +2575,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.paint_tool.run(toggle=True)
                     self.app.paint_tool.run(toggle=True)
                     return
                     return
 
 
-                # Paint Tool
+                # QRCode Tool
                 if key == QtCore.Qt.Key_Q:
                 if key == QtCore.Qt.Key_Q:
-                    self.app.pdf_tool.run()
+                    self.app.qrcode_tool.run()
                     return
                     return
 
 
                 # Rules Tool
                 # Rules Tool
@@ -2519,9 +2590,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_view_source()
                     self.app.on_view_source()
                     return
                     return
 
 
-                # Cutout Tool
-                if key == QtCore.Qt.Key_U:
-                    self.app.cutout_tool.run(toggle=True)
+                # Transformation Tool
+                if key == QtCore.Qt.Key_T:
+                    self.app.transform_tool.run(toggle=True)
                     return
                     return
 
 
                 # Substract Tool
                 # Substract Tool
@@ -2529,6 +2600,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.sub_tool.run(toggle=True)
                     self.app.sub_tool.run(toggle=True)
                     return
                     return
 
 
+                # Cutout Tool
+                if key == QtCore.Qt.Key_X:
+                    self.app.cutout_tool.run(toggle=True)
+                    return
+
                 # Panelize Tool
                 # Panelize Tool
                 if key == QtCore.Qt.Key_Z:
                 if key == QtCore.Qt.Key_Z:
                     self.app.panelize_tool.run(toggle=True)
                     self.app.panelize_tool.run(toggle=True)
@@ -2571,6 +2647,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 # It's meant to make a difference between delete objects and delete tools in
                 # It's meant to make a difference between delete objects and delete tools in
                 # Geometry Selected tool table
                 # Geometry Selected tool table
                 if key == QtCore.Qt.Key_Delete and matplotlib_key_flag is False:
                 if key == QtCore.Qt.Key_Delete and matplotlib_key_flag is False:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_delete()
+                        return
+
                     self.app.on_delete_keypress()
                     self.app.on_delete_keypress()
 
 
                 # Delete from canvas
                 # Delete from canvas
@@ -2663,6 +2746,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # Add a Tool from shortcut
                 # Add a Tool from shortcut
                 if key == QtCore.Qt.Key_T:
                 if key == QtCore.Qt.Key_T:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_add()
+                        return
+
                     self.app.on_tool_add_keypress()
                     self.app.on_tool_add_keypress()
 
 
                 # Zoom Fit
                 # Zoom Fit
@@ -2720,7 +2810,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
                         messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
                         messagebox.exec_()
                         messagebox.exec_()
                     return
                     return
-
             elif modifiers == QtCore.Qt.ShiftModifier:
             elif modifiers == QtCore.Qt.ShiftModifier:
                 # Run Distance Minimum Tool
                 # Run Distance Minimum Tool
                 if key == QtCore.Qt.Key_M or key == 'M':
                 if key == QtCore.Qt.Key_M or key == 'M':
@@ -2810,10 +2899,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                     self.app.geo_editor.transform_tool.on_rotate_key()
                     self.app.geo_editor.transform_tool.on_rotate_key()
 
 
+                # Zoom Out
                 if key == QtCore.Qt.Key_Minus or key == '-':
                 if key == QtCore.Qt.Key_Minus or key == '-':
                     self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
                     self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
 
 
+                # Zoom In
                 if key == QtCore.Qt.Key_Equal or key == '=':
                 if key == QtCore.Qt.Key_Equal or key == '=':
                     self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
                     self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
@@ -3382,16 +3473,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     val, ok = tool_add_popup.get_value()
                     val, ok = tool_add_popup.get_value()
                     if ok:
                     if ok:
                         self.app.exc_editor.on_tool_add(tooldia=val)
                         self.app.exc_editor.on_tool_add(tooldia=val)
-                        formated_val = '%.4f' % float(val)
-                        self.app.inform.emit('[success] %s: %s %s' %
-                                             (_("Added new tool with dia"),
-                                              formated_val,
-                                              str(self.units)
-                                              )
-                                             )
-                    else:
+                        formated_val = '%.*f' % (self.decimals, float(val))
                         self.app.inform.emit(
                         self.app.inform.emit(
-                            '[WARNING_NOTCL] %s' % _("Adding Tool cancelled ..."))
+                            '[success] %s: %s %s' % (_("Added new tool with dia"), formated_val, str(self.units))
+                        )
+                    else:
+                        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Adding Tool cancelled ..."))
                     return
                     return
 
 
                 # Zoom Fit
                 # Zoom Fit
@@ -3444,6 +3531,54 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 # Jump to coords
                 # Jump to coords
                 if key == QtCore.Qt.Key_J or key == 'J':
                 if key == QtCore.Qt.Key_J or key == 'J':
                     self.app.on_jump_to()
                     self.app.on_jump_to()
+        elif self.app.call_source == 'qrcode_tool':
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
+                if key == QtCore.Qt.Key_X:
+                    self.app.abort_all_tasks()
+                    return
+
+            elif modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Escape = Deselect All
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.qrcode_tool.on_exit()
+
+                # Grid toggle
+                if key == QtCore.Qt.Key_G:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_jump_to()
+        elif self.app.call_source == 'copper_thieving_tool':
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
+                if key == QtCore.Qt.Key_X:
+                    self.app.abort_all_tasks()
+                    return
+
+            elif modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Escape = Deselect All
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.copperfill_tool.on_exit()
+
+                # Grid toggle
+                if key == QtCore.Qt.Key_G:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_jump_to()
 
 
     def createPopupMenu(self):
     def createPopupMenu(self):
         menu = super().createPopupMenu()
         menu = super().createPopupMenu()
@@ -3557,9 +3692,27 @@ class FlatCAMActivityView(QtWidgets.QWidget):
     This class create and control the activity icon displayed in the App status bar
     This class create and control the activity icon displayed in the App status bar
     """
     """
 
 
-    def __init__(self, movie="share/active.gif", icon='share/active_static.png', parent=None):
+    def __init__(self, app, parent=None):
         super().__init__(parent=parent)
         super().__init__(parent=parent)
 
 
+        self.app = app
+
+        if self.app.defaults["global_activity_icon"] == "Ball green":
+            icon = 'share/active_2_static.png'
+            movie = "share/active_2.gif"
+        elif self.app.defaults["global_activity_icon"] == "Ball black":
+            icon = 'share/active_static.png'
+            movie = "share/active.gif"
+        elif self.app.defaults["global_activity_icon"] == "Arrow green":
+            icon = 'share/active_3_static.png'
+            movie = "share/active_3.gif"
+        elif self.app.defaults["global_activity_icon"] == "Eclipse green":
+            icon = 'share/active_4_static.png'
+            movie = "share/active_4.gif"
+        else:
+            icon = 'share/active_static.png'
+            movie = "share/active.gif"
+
         self.setMinimumWidth(200)
         self.setMinimumWidth(200)
         self.movie_path = movie
         self.movie_path = movie
         self.icon_path = icon
         self.icon_path = icon
@@ -3705,377 +3858,4 @@ class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon):
 
 
         exitAction.triggered.connect(self.app.final_save)
         exitAction.triggered.connect(self.app.final_save)
 
 
-
-class BookmarkManager(QtWidgets.QWidget):
-
-    mark_rows = QtCore.pyqtSignal()
-
-    def __init__(self, app, storage, parent=None):
-        super(BookmarkManager, self).__init__(parent)
-
-        self.app = app
-
-        assert isinstance(storage, dict), "Storage argument is not a dictionary"
-
-        self.bm_dict = deepcopy(storage)
-
-        # Icon and title
-        # self.setWindowIcon(parent.app_icon)
-        # self.setWindowTitle(_("Bookmark Manager"))
-        # self.resize(600, 400)
-
-        # title = QtWidgets.QLabel(
-        #     "<font size=8><B>FlatCAM</B></font><BR>"
-        # )
-        # title.setOpenExternalLinks(True)
-
-        # layouts
-        layout = QtWidgets.QVBoxLayout()
-        self.setLayout(layout)
-
-        table_hlay = QtWidgets.QHBoxLayout()
-        layout.addLayout(table_hlay)
-
-        self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1])
-        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
-        table_hlay.addWidget(self.table_widget)
-
-        self.table_widget.setColumnCount(3)
-        self.table_widget.setColumnWidth(0, 20)
-        self.table_widget.setHorizontalHeaderLabels(
-            [
-                '#',
-                _('Title'),
-                _('Web Link')
-            ]
-        )
-        self.table_widget.horizontalHeaderItem(0).setToolTip(
-            _("Index.\n"
-              "The rows in gray color will populate the Bookmarks menu.\n"
-              "The number of gray colored rows is set in Preferences."))
-        self.table_widget.horizontalHeaderItem(1).setToolTip(
-            _("Description of the link that is set as an menu action.\n"
-              "Try to keep it short because it is installed as a menu item."))
-        self.table_widget.horizontalHeaderItem(2).setToolTip(
-            _("Web Link. E.g: https://your_website.org "))
-
-        # pal = QtGui.QPalette()
-        # pal.setColor(QtGui.QPalette.Background, Qt.white)
-
-        # New Bookmark
-        new_vlay = QtWidgets.QVBoxLayout()
-        layout.addLayout(new_vlay)
-
-        new_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Bookmark"))
-        new_vlay.addWidget(new_title_lbl)
-
-        form0 = QtWidgets.QFormLayout()
-        new_vlay.addLayout(form0)
-
-        title_lbl = QtWidgets.QLabel('%s:' % _("Title"))
-        self.title_entry = FCEntry()
-        form0.addRow(title_lbl, self.title_entry)
-
-        link_lbl = QtWidgets.QLabel('%s:' % _("Web Link"))
-        self.link_entry = FCEntry()
-        self.link_entry.set_value('http://')
-        form0.addRow(link_lbl, self.link_entry)
-
-        # Buttons Layout
-        button_hlay = QtWidgets.QHBoxLayout()
-        layout.addLayout(button_hlay)
-
-        add_entry_btn = FCButton(_("Add Entry"))
-        remove_entry_btn = FCButton(_("Remove Entry"))
-        export_list_btn = FCButton(_("Export List"))
-        import_list_btn = FCButton(_("Import List"))
-        closebtn = QtWidgets.QPushButton(_("Close"))
-
-        # button_hlay.addStretch()
-        button_hlay.addWidget(add_entry_btn)
-        button_hlay.addWidget(remove_entry_btn)
-
-        button_hlay.addWidget(export_list_btn)
-        button_hlay.addWidget(import_list_btn)
-        # button_hlay.addWidget(closebtn)
-        # ##############################################################################
-        # ######################## SIGNALS #############################################
-        # ##############################################################################
-
-        add_entry_btn.clicked.connect(self.on_add_entry)
-        remove_entry_btn.clicked.connect(self.on_remove_entry)
-        export_list_btn.clicked.connect(self.on_export_bookmarks)
-        import_list_btn.clicked.connect(self.on_import_bookmarks)
-        self.title_entry.returnPressed.connect(self.on_add_entry)
-        self.link_entry.returnPressed.connect(self.on_add_entry)
-        # closebtn.clicked.connect(self.accept)
-
-        self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
-        self.build_bm_ui()
-
-    def build_bm_ui(self):
-
-        self.table_widget.setRowCount(len(self.bm_dict))
-
-        nr_crt = 0
-        sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0]))
-        for entry, bookmark in sorted_bookmarks:
-            row = nr_crt
-            nr_crt += 1
-
-            title = bookmark[0]
-            weblink = bookmark[1]
-
-            id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
-            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.table_widget.setItem(row, 0, id_item)  # Tool name/id
-
-            title_item = QtWidgets.QTableWidgetItem(title)
-            self.table_widget.setItem(row, 1, title_item)
-
-            weblink_txt = QtWidgets.QTextBrowser()
-            weblink_txt.setOpenExternalLinks(True)
-            weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame)
-            weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }")
-
-            weblink_txt.setHtml('<a href=%s>%s</a>' % (weblink, weblink))
-
-            self.table_widget.setCellWidget(row, 2, weblink_txt)
-
-            vertical_header = self.table_widget.verticalHeader()
-            vertical_header.hide()
-
-            horizontal_header = self.table_widget.horizontalHeader()
-            horizontal_header.setMinimumSectionSize(10)
-            horizontal_header.setDefaultSectionSize(70)
-            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-            horizontal_header.resizeSection(0, 20)
-            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
-            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
-
-        self.mark_table_rows_for_actions()
-
-        self.app.defaults["global_bookmarks"].clear()
-        for key, val in self.bm_dict.items():
-            self.app.defaults["global_bookmarks"][key] = deepcopy(val)
-
-    def on_add_entry(self, **kwargs):
-        """
-        Add a entry in the Bookmark Table and in the menu actions
-        :return: None
-        """
-        if 'title' in kwargs:
-            title = kwargs['title']
-        else:
-            title = self.title_entry.get_value()
-        if title == '':
-            self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}')
-            return 'fail'
-
-        if 'link' is kwargs:
-            link = kwargs['link']
-        else:
-            link = self.link_entry.get_value()
-
-        if link == 'http://':
-            self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}')
-            return 'fail'
-
-        # if 'http' not in link or 'https' not in link:
-        #     link = 'http://' + link
-
-        for bookmark in self.bm_dict.values():
-            if title == bookmark[0] or link == bookmark[1]:
-                self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}')
-                return 'fail'
-
-        # for some reason if the last char in the weblink is a slash it does not make the link clickable
-        # so I remove it
-        if link[-1] == '/':
-            link = link[:-1]
-        # add the new entry to storage
-        new_entry = len(self.bm_dict) + 1
-        self.bm_dict[str(new_entry)] = [title, link]
-
-        # add the link to the menu but only if it is within the set limit
-        bm_limit = int(self.app.defaults["global_bookmarks_limit"])
-        if len(self.bm_dict) < bm_limit:
-            act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks)
-            act.setText(title)
-            act.setIcon(QtGui.QIcon('share/link16.png'))
-            act.triggered.connect(lambda: webbrowser.open(link))
-            self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act)
-
-        self.app.inform.emit(f'[success] {_("Bookmark added.")}')
-
-        # add the new entry to the bookmark manager table
-        self.build_bm_ui()
-
-    def on_remove_entry(self):
-        """
-        Remove an Entry in the Bookmark table and from the menu actions
-        :return:
-        """
-        index_list = []
-        for model_index in self.table_widget.selectionModel().selectedRows():
-            index = QtCore.QPersistentModelIndex(model_index)
-            index_list.append(index)
-            title_to_remove = self.table_widget.item(model_index.row(), 1).text()
-
-            if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site':
-                self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed"))
-                self.build_bm_ui()
-                return
-            else:
-                for k, bookmark in list(self.bm_dict.items()):
-                    if title_to_remove == bookmark[0]:
-                        # remove from the storage
-                        self.bm_dict.pop(k, None)
-
-                        for act in self.app.ui.menuhelp_bookmarks.actions():
-                            if act.text() == title_to_remove:
-                                # disconnect the signal
-                                try:
-                                    act.triggered.disconnect()
-                                except TypeError:
-                                    pass
-                                # remove the action from the menu
-                                self.app.ui.menuhelp_bookmarks.removeAction(act)
-
-        # house keeping: it pays to have keys increased by one
-        new_key = 0
-        new_dict = dict()
-        for k, v in self.bm_dict.items():
-            # we start with key 1 so we can use the len(self.bm_dict)
-            # when adding bookmarks (keys in bm_dict)
-            new_key += 1
-            new_dict[str(new_key)] = v
-
-        self.bm_dict = deepcopy(new_dict)
-        new_dict.clear()
-
-        self.app.inform.emit(f'[success] {_("Bookmark removed.")}')
-
-        # for index in index_list:
-        #     self.table_widget.model().removeRow(index.row())
-        self.build_bm_ui()
-
-    def on_export_bookmarks(self):
-        self.app.report_usage("on_export_bookmarks")
-        self.app.log.debug("on_export_bookmarks()")
-
-        date = str(datetime.today()).rpartition('.')[0]
-        date = ''.join(c for c in date if c not in ':-')
-        date = date.replace(' ', '_')
-
-        filter__ = "Text File (*.TXT);;All Files (*.*)"
-        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"),
-                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
-                                                                 l_save=str(self.app.get_last_save_folder()),
-                                                                 n=_("Bookmarks"),
-                                                                 date=date),
-                                                             filter=filter__)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled."))
-            return
-        else:
-            try:
-                f = open(filename, 'w')
-                f.close()
-            except PermissionError:
-                self.app.inform.emit('[WARNING] %s' %
-                                     _("Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                return
-            except IOError:
-                self.app.log.debug('Creating a new bookmarks file ...')
-                f = open(filename, 'w')
-                f.close()
-            except:
-                e = sys.exc_info()[0]
-                self.app.log.error("Could not load defaults file.")
-                self.app.log.error(str(e))
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Could not load bookmarks file."))
-                return
-
-            # Save update options
-            try:
-                with open(filename, "w") as f:
-                    for title, link in self.bm_dict.items():
-                        line2write = str(title) + ':' + str(link) + '\n'
-                        f.write(line2write)
-            except:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Failed to write bookmarks to file."))
-                return
-        self.app.inform.emit('[success] %s: %s' %
-                             (_("Exported bookmarks to"), filename))
-
-    def on_import_bookmarks(self):
-        self.app.log.debug("on_import_bookmarks()")
-
-        filter_ = "Text File (*.txt);;All Files (*.*)"
-        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"),
-                                                             filter=filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("FlatCAM bookmarks import cancelled."))
-        else:
-            try:
-                with open(filename) as f:
-                    bookmarks = f.readlines()
-            except IOError:
-                self.app.log.error("Could not load bookmarks file.")
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Could not load bookmarks file."))
-                return
-
-            for line in bookmarks:
-                proc_line = line.replace(' ', '').partition(':')
-                self.on_add_entry(title=proc_line[0], link=proc_line[2])
-
-            self.app.inform.emit('[success] %s: %s' %
-                                 (_("Imported Bookmarks from"), filename))
-
-    def mark_table_rows_for_actions(self):
-        for row in range(self.table_widget.rowCount()):
-            item_to_paint = self.table_widget.item(row, 0)
-            if row < self.app.defaults["global_bookmarks_limit"]:
-                item_to_paint.setBackground(QtGui.QColor('gray'))
-                # item_to_paint.setForeground(QtGui.QColor('black'))
-            else:
-                item_to_paint.setBackground(QtGui.QColor('white'))
-                # item_to_paint.setForeground(QtGui.QColor('black'))
-
-    def rebuild_actions(self):
-        # rebuild the storage to reflect the order of the lines
-        self.bm_dict.clear()
-        for row in range(self.table_widget.rowCount()):
-            title = self.table_widget.item(row, 1).text()
-            wlink = self.table_widget.cellWidget(row, 2).toPlainText()
-
-            entry = int(row) + 1
-            self.bm_dict.update(
-                {
-                    str(entry): [title, wlink]
-                }
-            )
-
-        self.app.install_bookmarks(book_dict=self.bm_dict)
-
-    # def accept(self):
-    #     self.rebuild_actions()
-    #     super().accept()
-
-    def closeEvent(self, QCloseEvent):
-        self.rebuild_actions()
-        super().closeEvent(QCloseEvent)
-
 # end of file
 # end of file

+ 424 - 33
flatcamGUI/GUIElements.py

@@ -12,7 +12,7 @@
 # ##########################################################
 # ##########################################################
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt, pyqtSlot
+from PyQt5.QtCore import Qt, pyqtSlot, QSettings
 from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
 from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
 from PyQt5.QtGui import QKeySequence, QTextCursor
 from PyQt5.QtGui import QKeySequence, QTextCursor
 
 
@@ -145,7 +145,7 @@ class RadioSet(QtWidgets.QWidget):
 
 
 
 
 class LengthEntry(QtWidgets.QLineEdit):
 class LengthEntry(QtWidgets.QLineEdit):
-    def __init__(self, output_units='IN', parent=None):
+    def __init__(self, output_units='IN', decimals=None, parent=None):
         super(LengthEntry, self).__init__(parent)
         super(LengthEntry, self).__init__(parent)
 
 
         self.output_units = output_units
         self.output_units = output_units
@@ -160,6 +160,7 @@ class LengthEntry(QtWidgets.QLineEdit):
         }
         }
         self.readyToEdit = True
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
@@ -199,12 +200,13 @@ class LengthEntry(QtWidgets.QLineEdit):
         except KeyError:
         except KeyError:
             value = raw
             value = raw
             return float(eval(value))
             return float(eval(value))
-        except:
+        except Exception:
             log.warning("Could not parse value in entry: %s" % str(raw))
             log.warning("Could not parse value in entry: %s" % str(raw))
             return None
             return None
 
 
-    def set_value(self, val):
-        self.setText(str('%.4f' % val))
+    def set_value(self, val, decimals=None):
+        dec_digits = decimals if decimals is not None else self.decimals
+        self.setText(str('%.*f' % (dec_digits, val)))
 
 
     def sizeHint(self):
     def sizeHint(self):
         default_hint_size = super(LengthEntry, self).sizeHint()
         default_hint_size = super(LengthEntry, self).sizeHint()
@@ -212,10 +214,11 @@ class LengthEntry(QtWidgets.QLineEdit):
 
 
 
 
 class FloatEntry(QtWidgets.QLineEdit):
 class FloatEntry(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, decimals=None, parent=None):
         super(FloatEntry, self).__init__(parent)
         super(FloatEntry, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
@@ -251,9 +254,10 @@ class FloatEntry(QtWidgets.QLineEdit):
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
             return None
             return None
 
 
-    def set_value(self, val):
+    def set_value(self, val, decimals=None):
+        dig_digits = decimals if decimals is not None else self.decimals
         if val is not None:
         if val is not None:
-            self.setText("%.4f" % float(val))
+            self.setText("%.*f" % (dig_digits, float(val)))
         else:
         else:
             self.setText("")
             self.setText("")
 
 
@@ -263,10 +267,11 @@ class FloatEntry(QtWidgets.QLineEdit):
 
 
 
 
 class FloatEntry2(QtWidgets.QLineEdit):
 class FloatEntry2(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, decimals=None, parent=None):
         super(FloatEntry2, self).__init__(parent)
         super(FloatEntry2, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
@@ -295,8 +300,9 @@ class FloatEntry2(QtWidgets.QLineEdit):
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
             return None
             return None
 
 
-    def set_value(self, val):
-        self.setText("%.4f" % val)
+    def set_value(self, val, decimals=None):
+        dig_digits = decimals if decimals is not None else self.decimals
+        self.setText("%.*f" % (dig_digits, val))
 
 
     def sizeHint(self):
     def sizeHint(self):
         default_hint_size = super(FloatEntry2, self).sizeHint()
         default_hint_size = super(FloatEntry2, self).sizeHint()
@@ -353,10 +359,20 @@ class IntEntry(QtWidgets.QLineEdit):
 
 
 
 
 class FCEntry(QtWidgets.QLineEdit):
 class FCEntry(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, decimals=None, alignment=None, parent=None):
         super(FCEntry, self).__init__(parent)
         super(FCEntry, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
+            else:
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
@@ -376,9 +392,10 @@ class FCEntry(QtWidgets.QLineEdit):
     def get_value(self):
     def get_value(self):
         return str(self.text())
         return str(self.text())
 
 
-    def set_value(self, val):
+    def set_value(self, val, decimals=None):
+        decimal_digits = decimals if decimals is not None else self.decimals
         if type(val) is float:
         if type(val) is float:
-            self.setText('%.4f' % val)
+            self.setText('%.*f' % (decimal_digits, val))
         else:
         else:
             self.setText(str(val))
             self.setText(str(val))
 
 
@@ -511,20 +528,37 @@ class FCSpinner(QtWidgets.QSpinBox):
 
 
     returnPressed = QtCore.pyqtSignal()
     returnPressed = QtCore.pyqtSignal()
 
 
-    def __init__(self, parent=None):
+    def __init__(self, suffix=None, alignment=None, parent=None):
         super(FCSpinner, self).__init__(parent)
         super(FCSpinner, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
+
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
         self.lineEdit().installEventFilter(self)
         self.lineEdit().installEventFilter(self)
 
 
-    def eventFilter(self, object, event):
-        if event.type() == QtCore.QEvent.MouseButtonPress:
-            if self.readyToEdit:
-                self.lineEdit().selectAll()
-                self.readyToEdit = False
+        if suffix:
+            self.setSuffix(' %s' % str(suffix))
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
             else:
             else:
-                self.lineEdit().deselect()
-            return True
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
+
+        self.prev_readyToEdit = True
+
+    def eventFilter(self, object, event):
+        if event.type() == QtCore.QEvent.MouseButtonPress and self.prev_readyToEdit is True:
+            self.prev_readyToEdit = False
+            if self.isEnabled():
+                if self.readyToEdit:
+                    self.lineEdit().selectAll()
+                    self.readyToEdit = False
+                else:
+                    self.lineEdit().deselect()
+                return True
         return False
         return False
 
 
     def keyPressEvent(self, event):
     def keyPressEvent(self, event):
@@ -541,6 +575,7 @@ class FCSpinner(QtWidgets.QSpinBox):
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
+        self.returnPressed.emit()
 
 
     # def mousePressEvent(self, e, parent=None):
     # def mousePressEvent(self, e, parent=None):
     #     super(FCSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
     #     super(FCSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
@@ -554,6 +589,7 @@ class FCSpinner(QtWidgets.QSpinBox):
             super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
             super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
             self.lineEdit().deselect()
             self.lineEdit().deselect()
             self.readyToEdit = True
             self.readyToEdit = True
+            self.prev_readyToEdit = True
 
 
     def get_value(self):
     def get_value(self):
         return int(self.value())
         return int(self.value())
@@ -578,7 +614,7 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
 
 
     returnPressed = QtCore.pyqtSignal()
     returnPressed = QtCore.pyqtSignal()
 
 
-    def __init__(self, parent=None):
+    def __init__(self, suffix=None, alignment=None, parent=None):
         super(FCDoubleSpinner, self).__init__(parent)
         super(FCDoubleSpinner, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
 
 
@@ -590,17 +626,35 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
         self.lineEdit().setValidator(
         self.lineEdit().setValidator(
             QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
             QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
 
 
+        if suffix:
+            self.setSuffix(' %s' % str(suffix))
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
+            else:
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
+
+        self.prev_readyToEdit = True
+
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         self.clearFocus()
+        self.returnPressed.emit()
 
 
     def eventFilter(self, object, event):
     def eventFilter(self, object, event):
-        if event.type() == QtCore.QEvent.MouseButtonPress:
-            if self.readyToEdit:
-                self.lineEdit().selectAll()
-                self.readyToEdit = False
-            else:
-                self.lineEdit().deselect()
-            return True
+        if event.type() == QtCore.QEvent.MouseButtonPress and self.prev_readyToEdit is True:
+            self.prev_readyToEdit = False
+            if self.isEnabled():
+                if self.readyToEdit:
+                    self.cursor_pos = self.lineEdit().cursorPosition()
+                    self.lineEdit().selectAll()
+                    self.readyToEdit = False
+                else:
+                    self.lineEdit().deselect()
+                return True
         return False
         return False
 
 
     def keyPressEvent(self, event):
     def keyPressEvent(self, event):
@@ -621,6 +675,7 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
             super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
             super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
             self.lineEdit().deselect()
             self.lineEdit().deselect()
             self.readyToEdit = True
             self.readyToEdit = True
+            self.prev_readyToEdit = True
 
 
     def valueFromText(self, p_str):
     def valueFromText(self, p_str):
         text = p_str.replace(',', '.')
         text = p_str.replace(',', '.')
@@ -632,9 +687,10 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
         return ret_val
         return ret_val
 
 
     def validate(self, p_str, p_int):
     def validate(self, p_str, p_int):
+        text = p_str.replace(',', '.')
         try:
         try:
-            if float(p_str) < self.minimum() or float(p_str) > self.maximum():
-                return QtGui.QValidator.Intermediate, p_str, p_int
+            if float(text) < self.minimum() or float(text) > self.maximum():
+                return QtGui.QValidator.Intermediate, text, p_int
         except ValueError:
         except ValueError:
             pass
             pass
         return QtGui.QValidator.Acceptable, p_str, p_int
         return QtGui.QValidator.Acceptable, p_str, p_int
@@ -881,6 +937,178 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
         self.setTextCursor(cursor)
         self.setTextCursor(cursor)
 
 
 
 
+class FCPlainTextAreaExtended(QtWidgets.QPlainTextEdit):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+
+        self.completer = MyCompleter()
+
+        self.model = QtCore.QStringListModel()
+        self.completer.setModel(self.model)
+        self.set_model_data(keyword_list=[])
+        self.completer.insertText.connect(self.insertCompletion)
+
+        self.completer_enable = False
+
+    def append(self, text):
+        """
+        Added this to make this subclass compatible with FCTextAreaExtended
+        :param text: string
+        :return:
+        """
+        self.appendPlainText(text)
+
+    def set_model_data(self, keyword_list):
+        self.model.setStringList(keyword_list)
+
+    def insertCompletion(self, completion):
+        tc = self.textCursor()
+        extra = (len(completion) - len(self.completer.completionPrefix()))
+
+        # don't insert if the word is finished but add a space instead
+        if extra == 0:
+            tc.insertText(' ')
+            self.completer.popup().hide()
+            return
+
+        tc.movePosition(QTextCursor.Left)
+        tc.movePosition(QTextCursor.EndOfWord)
+        tc.insertText(completion[-extra:])
+        # add a space after inserting the word
+        tc.insertText(' ')
+        self.setTextCursor(tc)
+        self.completer.popup().hide()
+
+    def focusInEvent(self, event):
+        if self.completer:
+            self.completer.setWidget(self)
+        QtWidgets.QPlainTextEdit.focusInEvent(self, event)
+
+    def set_value(self, val):
+        self.setPlainText(val)
+
+    def get_value(self):
+        self.toPlainText()
+
+    def insertFromMimeData(self, data):
+        """
+        Reimplemented such that when SHIFT is pressed and doing click Paste in the contextual menu, the '\' symbol
+        is replaced with the '/' symbol. That's because of the difference in path separators in Windows and TCL
+        :param data:
+        :return:
+        """
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+        if modifier == Qt.ShiftModifier:
+            text = data.text()
+            text = text.replace('\\', '/')
+            self.insertPlainText(text)
+        else:
+            self.insertPlainText(data.text())
+
+    def keyPressEvent(self, event):
+        """
+        Reimplemented so the CTRL + SHIFT + V shortcut key combo will paste the text but replacing '\' with '/'
+        :param event:
+        :return:
+        """
+        key = event.key()
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if modifier & Qt.ControlModifier and modifier & Qt.ShiftModifier:
+            if key == QtCore.Qt.Key_V:
+                clipboard = QtWidgets.QApplication.clipboard()
+                clip_text = clipboard.text()
+                clip_text = clip_text.replace('\\', '/')
+                self.insertPlainText(clip_text)
+
+        if modifier & Qt.ControlModifier and key == Qt.Key_Slash:
+            self.comment()
+
+        tc = self.textCursor()
+        if (key == Qt.Key_Tab or key == Qt.Key_Enter or key == Qt.Key_Return) and self.completer.popup().isVisible():
+            self.completer.insertText.emit(self.completer.getSelected())
+            self.completer.setCompletionMode(QCompleter.PopupCompletion)
+            return
+        elif key == Qt.Key_BraceLeft:
+            tc.insertText('{}')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_BracketLeft:
+            tc.insertText('[]')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_ParenLeft:
+            tc.insertText('()')
+            self.moveCursor(QtGui.QTextCursor.Left)
+
+        elif key == Qt.Key_BraceRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == '}':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText('}')
+        elif key == Qt.Key_BracketRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ']':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(']')
+        elif key == Qt.Key_ParenRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ')':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(')')
+        else:
+            super(FCPlainTextAreaExtended, self).keyPressEvent(event)
+
+        if self.completer_enable:
+            tc.select(QTextCursor.WordUnderCursor)
+            cr = self.cursorRect()
+
+            if len(tc.selectedText()) > 0:
+                self.completer.setCompletionPrefix(tc.selectedText())
+                popup = self.completer.popup()
+                popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+                cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+                            + self.completer.popup().verticalScrollBar().sizeHint().width())
+                self.completer.complete(cr)
+            else:
+                self.completer.popup().hide()
+
+    def comment(self):
+        """
+        Got it from here:
+        https://stackoverflow.com/questions/49898820/how-to-get-text-next-to-cursor-in-qtextedit-in-pyqt4
+        :return:
+        """
+        pos = self.textCursor().position()
+        self.moveCursor(QtGui.QTextCursor.StartOfLine)
+        line_text = self.textCursor().block().text()
+        if self.textCursor().block().text().startswith(" "):
+            # skip the white space
+            self.moveCursor(QtGui.QTextCursor.NextWord)
+        self.moveCursor(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor)
+        character = self.textCursor().selectedText()
+        if character == "#":
+            # delete #
+            self.textCursor().deletePreviousChar()
+            # delete white space
+            self.moveCursor(QtGui.QTextCursor.NextWord, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().removeSelectedText()
+        else:
+            self.moveCursor(QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().insertText("# ")
+        cursor = QtGui.QTextCursor(self.textCursor())
+        cursor.setPosition(pos)
+        self.setTextCursor(cursor)
+
+
 class FCComboBox(QtWidgets.QComboBox):
 class FCComboBox(QtWidgets.QComboBox):
 
 
     def __init__(self, parent=None, callback=None):
     def __init__(self, parent=None, callback=None):
@@ -966,6 +1194,17 @@ class FCButton(QtWidgets.QPushButton):
         self.setText(str(val))
         self.setText(str(val))
 
 
 
 
+class FCLabel(QtWidgets.QLabel):
+    def __init__(self, parent=None):
+        super(FCLabel, self).__init__(parent)
+
+    def get_value(self):
+        return self.text()
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+
 class FCMenu(QtWidgets.QMenu):
 class FCMenu(QtWidgets.QMenu):
     def __init__(self):
     def __init__(self):
         super().__init__()
         super().__init__()
@@ -1773,8 +2012,9 @@ class FCTable(QtWidgets.QTableWidget):
 
 
     # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
     # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
     def mousePressEvent(self, event):
     def mousePressEvent(self, event):
-        if self.itemAt(event.pos()) is None:
+        if not self.itemAt(event.pos()):
             self.clearSelection()
             self.clearSelection()
+            self.clearFocus()
         else:
         else:
             QtWidgets.QTableWidget.mousePressEvent(self, event)
             QtWidgets.QTableWidget.mousePressEvent(self, event)
 
 
@@ -2161,3 +2401,154 @@ class MyCompleter(QCompleter):
 
 
     def getSelected(self):
     def getSelected(self):
         return self.lastSelected
         return self.lastSelected
+
+
+class FCTextAreaLineNumber(QtWidgets.QFrame):
+    textChanged = QtCore.pyqtSignal()
+
+    class NumberBar(QtWidgets.QWidget):
+
+        def __init__(self, edit):
+            QtWidgets.QWidget.__init__(self, edit)
+
+            self.edit = edit
+            self.adjustWidth(1)
+
+        def paintEvent(self, event):
+            self.edit.numberbarPaint(self, event)
+            QtWidgets.QWidget.paintEvent(self, event)
+
+        def adjustWidth(self, count):
+            # three spaces added to the width to make up for the space added in the line number
+            width = self.fontMetrics().width(str(count) + '   ')
+            if self.width() != width:
+                self.setFixedWidth(width)
+
+        def updateContents(self, rect, scroll):
+            if scroll:
+                self.scroll(0, scroll)
+            else:
+                # It would be nice to do
+                # self.update(0, rect.y(), self.width(), rect.height())
+                # But we can't because it will not remove the bold on the
+                # current line if word wrap is enabled and a new block is
+                # selected.
+                self.update()
+
+    class PlainTextEdit(FCPlainTextAreaExtended):
+        """
+        TextEdit with line numbers and highlight
+        From here: https://nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/
+        and from here: https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
+        """
+
+        def __init__(self, *args):
+            FCPlainTextAreaExtended.__init__(self, *args)
+
+            # self.setFrameStyle(QFrame.NoFrame)
+            self.setFrameStyle(QtWidgets.QFrame.NoFrame)
+            self.highlight()
+            # self.setLineWrapMode(QPlainTextEdit.NoWrap)
+            self.cursorPositionChanged.connect(self.highlight)
+
+        def highlight(self):
+            hi_selection = QTextEdit.ExtraSelection()
+
+            hi_selection.format.setBackground(self.palette().alternateBase())
+            hi_selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
+            hi_selection.cursor = self.textCursor()
+            hi_selection.cursor.clearSelection()
+
+            self.setExtraSelections([hi_selection])
+
+        def numberbarPaint(self, number_bar, event):
+            font_metrics = self.fontMetrics()
+            current_line = self.document().findBlock(self.textCursor().position()).blockNumber() + 1
+
+            painter = QtGui.QPainter(number_bar)
+            painter.fillRect(event.rect(), QtCore.Qt.lightGray)
+
+            block = self.firstVisibleBlock()
+            line_count = int(block.blockNumber())
+            block_top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
+            block_bottom = block_top + int(self.blockBoundingRect(block).height())
+
+            # Iterate over all visible text blocks in the document.
+            while block.isValid() and block_top <= event.rect().bottom():
+                line_count += 1
+
+                # Check if the position of the block is out side of the visible
+                # area.
+                if block.isVisible() and block_bottom >= event.rect().top():
+
+                    # We want the line number for the selected line to be bold.
+                    if line_count == current_line:
+                        font = painter.font()
+                        font.setBold(True)
+                        painter.setPen(QtCore.Qt.blue)
+                        painter.setFont(font)
+                    else:
+                        font = painter.font()
+                        font.setBold(False)
+                        painter.setPen(self.palette().base().color())
+                        painter.setFont(font)
+
+                    # Draw the line number right justified at the position of the line.
+                    paint_rect = QtCore.QRect(0, block_top, number_bar.width(), font_metrics.height())
+                    # I add some spaces to the line_count to prettify; make sure to remember adjust the width in the
+                    # NumberBar() class above
+                    painter.drawText(paint_rect, Qt.AlignRight, ' ' + str(line_count) + '  ')
+
+                block = block.next()
+                block_top = block_bottom
+                block_bottom = block_top + self.blockBoundingRect(block).height()
+
+            painter.end()
+
+    def __init__(self, *args):
+        QtWidgets.QFrame.__init__(self, *args)
+
+        self.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken)
+
+        self.edit = self.PlainTextEdit()
+        self.number_bar = self.NumberBar(self.edit)
+
+        hbox = QtWidgets.QHBoxLayout(self)
+        hbox.setSpacing(0)
+        hbox.setContentsMargins(0, 0, 0, 0)
+        hbox.addWidget(self.number_bar)
+        hbox.addWidget(self.edit)
+
+        self.edit.blockCountChanged.connect(self.number_bar.adjustWidth)
+        self.edit.updateRequest.connect(self.number_bar.updateContents)
+
+    def getText(self):
+        return str(self.edit.toPlainText())
+
+    def setText(self, text):
+        self.edit.setPlainText(text)
+
+    def isModified(self):
+        return self.edit.document().isModified()
+
+    def setModified(self, modified):
+        self.edit.document().setModified(modified)
+
+    def setLineWrapMode(self, mode):
+        self.edit.setLineWrapMode(mode)
+
+
+def rreplace(s, old, new, occurrence):
+    """
+    Credits go here:
+    https://stackoverflow.com/questions/2556108/rreplace-how-to-replace-the-last-occurrence-of-an-expression-in-a-string
+
+    :param s: string to be processed
+    :param old: old char to be replaced
+    :param new: new char to replace the old one
+    :param occurrence: how many places from end to replace the old char
+    :return: modified string
+    """
+
+    li = s.rsplit(old, occurrence)
+    return new.join(li)

+ 260 - 159
flatcamGUI/ObjectUI.py

@@ -22,6 +22,12 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
 
 
 class ObjectUI(QtWidgets.QWidget):
 class ObjectUI(QtWidgets.QWidget):
     """
     """
@@ -29,10 +35,11 @@ class ObjectUI(QtWidgets.QWidget):
     put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
     put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
     """
     """
 
 
-    def __init__(self, icon_file='share/flatcam_icon32.png', title=_('FlatCAM Object'), parent=None, common=True):
+    def __init__(self, icon_file='share/flatcam_icon32.png', title=_('FlatCAM Object'), parent=None, common=True, 
+                 decimals=4):
         QtWidgets.QWidget.__init__(self, parent=parent)
         QtWidgets.QWidget.__init__(self, parent=parent)
 
 
-        self.decimals = 4
+        self.decimals = decimals
 
 
         layout = QtWidgets.QVBoxLayout()
         layout = QtWidgets.QVBoxLayout()
         self.setLayout(layout)
         self.setLayout(layout)
@@ -147,9 +154,9 @@ class GerberObjectUI(ObjectUI):
     User interface for Gerber objects.
     User interface for Gerber objects.
     """
     """
 
 
-    def __init__(self, parent=None):
-        ObjectUI.__init__(self, title=_('Gerber Object'), parent=parent)
-        self.decimals = 4
+    def __init__(self, decimals, parent=None):
+        ObjectUI.__init__(self, title=_('Gerber Object'), parent=parent, decimals=decimals)
+        self.decimals = decimals
 
 
         # Plot options
         # Plot options
         grid0 = QtWidgets.QGridLayout()
         grid0 = QtWidgets.QGridLayout()
@@ -275,6 +282,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid1)
         self.custom_box.addLayout(grid1)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(1, 1)
         grid1.setColumnStretch(1, 1)
+        grid1.setColumnStretch(2, 1)
 
 
         # Tool Type
         # Tool Type
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
@@ -284,8 +292,8 @@ class GerberObjectUI(ObjectUI):
               "When the 'V-shape' is selected then the tool\n"
               "When the 'V-shape' is selected then the tool\n"
               "diameter will depend on the chosen cut depth.")
               "diameter will depend on the chosen cut depth.")
         )
         )
-        self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'},
-                                         {'label': 'V-Shape', 'value': 'v'}])
+        self.tool_type_radio = RadioSet([{'label': _('Circular'), 'value': 'circular'},
+                                         {'label': _('V-Shape'), 'value': 'v'}])
 
 
         grid1.addWidget(self.tool_type_label, 0, 0)
         grid1.addWidget(self.tool_type_label, 0, 0)
         grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2)
         grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2)
@@ -342,7 +350,7 @@ class GerberObjectUI(ObjectUI):
         )
         )
         tdlabel.setMinimumWidth(90)
         tdlabel.setMinimumWidth(90)
         self.iso_tool_dia_entry = FCDoubleSpinner()
         self.iso_tool_dia_entry = FCDoubleSpinner()
-        self.iso_tool_dia_entry.set_range(0, 9999.9999)
+        self.iso_tool_dia_entry.set_range(-9999.9999, 9999.9999)
         self.iso_tool_dia_entry.set_precision(self.decimals)
         self.iso_tool_dia_entry.set_precision(self.decimals)
         self.iso_tool_dia_entry.setSingleStep(0.1)
         self.iso_tool_dia_entry.setSingleStep(0.1)
 
 
@@ -364,15 +372,13 @@ class GerberObjectUI(ObjectUI):
         # Pass overlap
         # Pass overlap
         overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
         overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
         overlabel.setToolTip(
         overlabel.setToolTip(
-            _("How much (fraction) of the tool width to overlap each tool pass.\n"
-              "Example:\n"
-              "A value here of 0.25 means an overlap of 25%% from the tool diameter found above.")
+            _("How much (fraction) of the tool width to overlap each tool pass.")
         )
         )
         overlabel.setMinimumWidth(90)
         overlabel.setMinimumWidth(90)
-        self.iso_overlap_entry = FCDoubleSpinner()
+        self.iso_overlap_entry = FCDoubleSpinner(suffix='%')
         self.iso_overlap_entry.set_precision(self.decimals)
         self.iso_overlap_entry.set_precision(self.decimals)
         self.iso_overlap_entry.setWrapping(True)
         self.iso_overlap_entry.setWrapping(True)
-        self.iso_overlap_entry.setRange(0.000, 0.999)
+        self.iso_overlap_entry.setRange(0.0000, 99.9999)
         self.iso_overlap_entry.setSingleStep(0.1)
         self.iso_overlap_entry.setSingleStep(0.1)
         grid1.addWidget(overlabel, 6, 0)
         grid1.addWidget(overlabel, 6, 0)
         grid1.addWidget(self.iso_overlap_entry, 6, 1, 1, 2)
         grid1.addWidget(self.iso_overlap_entry, 6, 1, 1, 2)
@@ -385,12 +391,12 @@ class GerberObjectUI(ObjectUI):
               "- conventional / useful when there is no backlash compensation")
               "- conventional / useful when there is no backlash compensation")
         )
         )
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'label': _('Conv.'), 'value': 'cv'}])
+                                            {'label': _('Conventional'), 'value': 'cv'}])
         grid1.addWidget(self.milling_type_label, 7, 0)
         grid1.addWidget(self.milling_type_label, 7, 0)
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
 
 
         # combine all passes CB
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
         self.combine_passes_cb.setToolTip(
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
             _("Combine all passes into one object")
         )
         )
@@ -400,15 +406,15 @@ class GerberObjectUI(ObjectUI):
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
                                     "This means that it will cut through\n"
                                     "This means that it will cut through\n"
                                     "the middle of the trace."))
                                     "the middle of the trace."))
+        grid1.addWidget(self.combine_passes_cb, 8, 0)
 
 
         # avoid an area from isolation
         # avoid an area from isolation
         self.except_cb = FCCheckBox(label=_('Except'))
         self.except_cb = FCCheckBox(label=_('Except'))
+        grid1.addWidget(self.follow_cb, 8, 1)
+
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object bellow\n"
                                     "by checking this, the area of the object bellow\n"
                                     "will be subtracted from the isolation geometry."))
                                     "will be subtracted from the isolation geometry."))
-
-        grid1.addWidget(self.combine_passes_cb, 8, 0)
-        grid1.addWidget(self.follow_cb, 8, 1)
         grid1.addWidget(self.except_cb, 8, 2)
         grid1.addWidget(self.except_cb, 8, 2)
 
 
         # ## Form Layout
         # ## Form Layout
@@ -448,8 +454,50 @@ class GerberObjectUI(ObjectUI):
 
 
         form_layout.addRow(self.obj_label, self.obj_combo)
         form_layout.addRow(self.obj_label, self.obj_combo)
 
 
-        self.gen_iso_label = QtWidgets.QLabel("<b>%s</b>" % _("Generate Isolation Geometry"))
-        self.gen_iso_label.setToolTip(
+        # ---------------------------------------------- #
+        # --------- Isolation scope -------------------- #
+        # ---------------------------------------------- #
+        self.iso_scope_label = QtWidgets.QLabel('<b>%s:</b>' % _('Scope'))
+        self.iso_scope_label.setToolTip(
+            _("Isolation scope. Choose what to isolate:\n"
+              "- 'All' -> Isolate all the polygons in the object\n"
+              "- 'Selection' -> Isolate a selection of polygons.")
+        )
+        self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'},
+                                         {'label': _('Selection'), 'value': 'single'}])
+
+        grid1.addWidget(self.iso_scope_label, 10, 0)
+        grid1.addWidget(self.iso_scope_radio, 10, 1, 1, 2)
+
+        # ---------------------------------------------- #
+        # --------- Isolation type  -------------------- #
+        # ---------------------------------------------- #
+        self.iso_type_label = QtWidgets.QLabel('<b>%s:</b>' % _('Isolation Type'))
+        self.iso_type_label.setToolTip(
+            _("Choose how the isolation will be executed:\n"
+              "- 'Full' -> complete isolation of polygons\n"
+              "- 'Ext' -> will isolate only on the outside\n"
+              "- 'Int' -> will isolate only on the inside\n"
+              "'Exterior' isolation is almost always possible\n"
+              "(with the right tool) but 'Interior'\n"
+              "isolation can be done only when there is an opening\n"
+              "inside of the polygon (e.g polygon is a 'doughnut' shape).")
+        )
+        self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
+                                        {'label': _('Ext'), 'value': 'ext'},
+                                        {'label': _('Int'), 'value': 'int'}])
+
+        grid1.addWidget(self.iso_type_label, 11, 0)
+        grid1.addWidget(self.iso_type_radio, 11, 1, 1, 2)
+
+        self.generate_iso_button = QtWidgets.QPushButton("%s" % _("Generate Isolation Geometry"))
+        self.generate_iso_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.generate_iso_button.setToolTip(
             _("Create a Geometry object with toolpaths to cut \n"
             _("Create a Geometry object with toolpaths to cut \n"
               "isolation outside, inside or on both sides of the\n"
               "isolation outside, inside or on both sides of the\n"
               "object. For a Gerber object outside means outside\n"
               "object. For a Gerber object outside means outside\n"
@@ -460,7 +508,7 @@ class GerberObjectUI(ObjectUI):
               "inside the actual Gerber feature, use a negative tool\n"
               "inside the actual Gerber feature, use a negative tool\n"
               "diameter above.")
               "diameter above.")
         )
         )
-        grid1.addWidget(self.gen_iso_label, 10, 0, 1, 3)
+        grid1.addWidget(self.generate_iso_button, 12, 0, 1, 3)
 
 
         self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry'))
         self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry'))
         self.create_buffer_button.setToolTip(
         self.create_buffer_button.setToolTip(
@@ -469,48 +517,15 @@ class GerberObjectUI(ObjectUI):
               "Clicking this will create the buffered geometry\n"
               "Clicking this will create the buffered geometry\n"
               "required for isolation.")
               "required for isolation.")
         )
         )
-        grid1.addWidget(self.create_buffer_button, 11, 0, 1, 3)
-
-        self.generate_iso_button = QtWidgets.QPushButton(_('FULL Geo'))
-        self.generate_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing. It contains both\n"
-              "the interiors and exteriors geometry.")
-        )
-        grid1.addWidget(self.generate_iso_button, 12, 0)
-
-        hlay_1 = QtWidgets.QHBoxLayout()
-        grid1.addLayout(hlay_1, 12, 1, 1, 2)
-
-        self.generate_ext_iso_button = QtWidgets.QPushButton(_('Ext Geo'))
-        self.generate_ext_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing containing\n"
-              "only the exteriors geometry.")
-        )
-        # self.generate_ext_iso_button.setMinimumWidth(100)
-        hlay_1.addWidget(self.generate_ext_iso_button)
-
-        self.generate_int_iso_button = QtWidgets.QPushButton(_('Int Geo'))
-        self.generate_int_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing containing\n"
-              "only the interiors geometry.")
-        )
-        # self.generate_ext_iso_button.setMinimumWidth(90)
-        hlay_1.addWidget(self.generate_int_iso_button)
+        grid1.addWidget(self.create_buffer_button, 13, 0, 1, 2)
 
 
         self.ohis_iso = OptionalHideInputSection(
         self.ohis_iso = OptionalHideInputSection(
             self.except_cb,
             self.except_cb,
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             logic=True
             logic=True
         )
         )
-        # when the follow checkbox is checked then the exteriors and interiors isolation generation buttons
-        # are disabled as is doesn't make sense to have them enabled due of the nature of "follow"
-        self.ois_iso = OptionalInputSection(self.follow_cb,
-                                            [self.generate_int_iso_button, self.generate_ext_iso_button], logic=False)
 
 
-        grid1.addWidget(QtWidgets.QLabel(''), 13, 0)
+        grid1.addWidget(QtWidgets.QLabel(''), 14, 0)
 
 
         # ###########################################
         # ###########################################
         # ########## NEW GRID #######################
         # ########## NEW GRID #######################
@@ -556,6 +571,11 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.board_cutout_label, 2, 0)
         grid2.addWidget(self.board_cutout_label, 2, 0)
         grid2.addWidget(self.generate_cutout_button, 2, 1)
         grid2.addWidget(self.generate_cutout_button, 2, 1)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 3, 0, 1, 2)
+
         # ## Non-copper regions
         # ## Non-copper regions
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label.setToolTip(
         self.noncopper_label.setToolTip(
@@ -566,7 +586,7 @@ class GerberObjectUI(ObjectUI):
               "copper from a specified region.")
               "copper from a specified region.")
         )
         )
 
 
-        grid2.addWidget(self.noncopper_label, 3, 0, 1, 2)
+        grid2.addWidget(self.noncopper_label, 4, 0, 1, 2)
 
 
         # Margin
         # Margin
         bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
@@ -582,8 +602,8 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_margin_entry.set_precision(self.decimals)
         self.noncopper_margin_entry.set_precision(self.decimals)
         self.noncopper_margin_entry.setSingleStep(0.1)
         self.noncopper_margin_entry.setSingleStep(0.1)
 
 
-        grid2.addWidget(bmlabel, 4, 0)
-        grid2.addWidget(self.noncopper_margin_entry, 4, 1)
+        grid2.addWidget(bmlabel, 5, 0)
+        grid2.addWidget(self.noncopper_margin_entry, 5, 1)
 
 
         # Rounded corners
         # Rounded corners
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
@@ -593,8 +613,13 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_rounded_cb.setMinimumWidth(90)
         self.noncopper_rounded_cb.setMinimumWidth(90)
 
 
         self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo'))
         self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo'))
-        grid2.addWidget(self.noncopper_rounded_cb, 5, 0)
-        grid2.addWidget(self.generate_noncopper_button, 5, 1)
+        grid2.addWidget(self.noncopper_rounded_cb, 6, 0)
+        grid2.addWidget(self.generate_noncopper_button, 6, 1)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line1, 7, 0, 1, 2)
 
 
         # ## Bounding box
         # ## Bounding box
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
@@ -603,7 +628,7 @@ class GerberObjectUI(ObjectUI):
               "Square shape.")
               "Square shape.")
         )
         )
 
 
-        grid2.addWidget(self.boundingbox_label, 6, 0, 1, 2)
+        grid2.addWidget(self.boundingbox_label, 8, 0, 1, 2)
 
 
         bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin.setToolTip(
         bbmargin.setToolTip(
@@ -616,8 +641,8 @@ class GerberObjectUI(ObjectUI):
         self.bbmargin_entry.set_precision(self.decimals)
         self.bbmargin_entry.set_precision(self.decimals)
         self.bbmargin_entry.setSingleStep(0.1)
         self.bbmargin_entry.setSingleStep(0.1)
 
 
-        grid2.addWidget(bbmargin, 7, 0)
-        grid2.addWidget(self.bbmargin_entry, 7, 1)
+        grid2.addWidget(bbmargin, 9, 0)
+        grid2.addWidget(self.bbmargin_entry, 9, 1)
 
 
         self.bbrounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.bbrounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.bbrounded_cb.setToolTip(
         self.bbrounded_cb.setToolTip(
@@ -632,21 +657,26 @@ class GerberObjectUI(ObjectUI):
         self.generate_bb_button.setToolTip(
         self.generate_bb_button.setToolTip(
             _("Generate the Geometry object.")
             _("Generate the Geometry object.")
         )
         )
-        grid2.addWidget(self.bbrounded_cb, 8, 0)
-        grid2.addWidget(self.generate_bb_button, 8, 1)
+        grid2.addWidget(self.bbrounded_cb, 10, 0)
+        grid2.addWidget(self.generate_bb_button, 10, 1)
 
 
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line2, 11, 0, 1, 2)
 
 
 class ExcellonObjectUI(ObjectUI):
 class ExcellonObjectUI(ObjectUI):
     """
     """
     User interface for Excellon objects.
     User interface for Excellon objects.
     """
     """
 
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         ObjectUI.__init__(self, title=_('Excellon Object'),
         ObjectUI.__init__(self, title=_('Excellon Object'),
                           icon_file='share/drill32.png',
                           icon_file='share/drill32.png',
-                          parent=parent)
+                          parent=parent,
+                          decimals=decimals)
 
 
-        self.decimals = 4
+        self.decimals = decimals
 
 
         # ### Plot options ####
         # ### Plot options ####
         hlay_plot = QtWidgets.QHBoxLayout()
         hlay_plot = QtWidgets.QHBoxLayout()
@@ -754,7 +784,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(cutzlabel, 0, 0)
         grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_precision(self.decimals)
-        self.cutz_entry.setRange(-9999.9999, -0.000001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-9999.9999, -0.000001)
+        else:
+            self.cutz_entry.setRange(-9999.9999, 9999.9999)
+
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.cutz_entry, 0, 1)
         grid1.addWidget(self.cutz_entry, 0, 1)
@@ -768,7 +803,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(travelzlabel, 1, 0)
         grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry.set_precision(self.decimals)
         self.travelz_entry.set_precision(self.decimals)
-        self.travelz_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.travelz_entry.setRange(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.setRange(-9999.9999, 9999.9999)
+
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.travelz_entry, 1, 1)
         grid1.addWidget(self.travelz_entry, 1, 1)
@@ -790,7 +830,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(toolchzlabel, 3, 0)
         grid1.addWidget(toolchzlabel, 3, 0)
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry.set_precision(self.decimals)
         self.toolchangez_entry.set_precision(self.decimals)
-        self.toolchangez_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.setRange(0.0, 9999.9999)
+        else:
+            self.toolchangez_entry.setRange(-9999.9999, 9999.9999)
+
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.toolchangez_entry, 3, 1)
         grid1.addWidget(self.toolchangez_entry, 3, 1)
@@ -815,7 +860,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.eendz_label, 5, 0)
         grid1.addWidget(self.eendz_label, 5, 0)
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry.set_precision(self.decimals)
         self.eendz_entry.set_precision(self.decimals)
-        self.eendz_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.eendz_entry.setRange(0.0, 9999.9999)
+        else:
+            self.eendz_entry.setRange(-9999.9999, 9999.9999)
+
         self.eendz_entry.setSingleStep(0.1)
         self.eendz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.eendz_entry, 5, 1)
         grid1.addWidget(self.eendz_entry, 5, 1)
@@ -885,10 +935,10 @@ class ExcellonObjectUI(ObjectUI):
 
 
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
 
-        # postprocessor selection
-        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Postprocessor"))
+        # preprocessor selection
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
         pp_excellon_label.setToolTip(
         pp_excellon_label.setToolTip(
-            _("The postprocessor JSON file that dictates\n"
+            _("The preprocessor JSON file that dictates\n"
               "Gcode output.")
               "Gcode output.")
         )
         )
         self.pp_excellon_name_cb = FCComboBox()
         self.pp_excellon_name_cb = FCComboBox()
@@ -934,12 +984,11 @@ class ExcellonObjectUI(ObjectUI):
         grid2.setColumnStretch(0, 0)
         grid2.setColumnStretch(0, 0)
         grid2.setColumnStretch(1, 1)
         grid2.setColumnStretch(1, 1)
 
 
-        choose_tools_label = QtWidgets.QLabel(
-            _("Select from the Tools Table above\n"
-              "the hole dias that are to be drilled.\n"
-              "Use the # column to make the selection.")
-        )
-        grid2.addWidget(choose_tools_label, 0, 0, 1, 3)
+        # choose_tools_label = QtWidgets.QLabel(
+        #     _("Select from the Tools Table above the hole dias to be\n"
+        #       "drilled. Use the # column to make the selection.")
+        # )
+        # grid2.addWidget(choose_tools_label, 0, 0, 1, 3)
 
 
         # ### Choose what to use for Gcode creation: Drills, Slots or Both
         # ### Choose what to use for Gcode creation: Drills, Slots or Both
         gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
         gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
@@ -967,17 +1016,12 @@ class ExcellonObjectUI(ObjectUI):
         # ### Milling Holes Drills ####
         # ### Milling Holes Drills ####
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label.setToolTip(
         self.mill_hole_label.setToolTip(
-            _("Create Geometry for milling holes.")
+            _("Create Geometry for milling holes.\n"
+              "Select from the Tools Table above the hole dias to be\n"
+              "milled. Use the # column to make the selection.")
         )
         )
         grid2.addWidget(self.mill_hole_label, 3, 0, 1, 3)
         grid2.addWidget(self.mill_hole_label, 3, 0, 1, 3)
 
 
-        self.choose_tools_label2 = QtWidgets.QLabel(
-            _("Select from the Tools Table above\n"
-              "the hole dias that are to be milled.\n"
-              "Use the # column to make the selection.")
-        )
-        grid2.addWidget(self.choose_tools_label2, 4, 0, 1, 3)
-
         self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel.setToolTip(
         self.tdlabel.setToolTip(
             _("Diameter of the cutting tool.")
             _("Diameter of the cutting tool.")
@@ -993,9 +1037,9 @@ class ExcellonObjectUI(ObjectUI):
               "for milling DRILLS toolpaths.")
               "for milling DRILLS toolpaths.")
         )
         )
 
 
-        grid2.addWidget(self.tdlabel, 5, 0)
-        grid2.addWidget(self.tooldia_entry, 5, 1)
-        grid2.addWidget(self.generate_milling_button, 5, 2)
+        grid2.addWidget(self.tdlabel, 4, 0)
+        grid2.addWidget(self.tooldia_entry, 4, 1)
+        grid2.addWidget(self.generate_milling_button, 4, 2)
 
 
         self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel.setToolTip(
         self.stdlabel.setToolTip(
@@ -1014,9 +1058,9 @@ class ExcellonObjectUI(ObjectUI):
               "for milling SLOTS toolpaths.")
               "for milling SLOTS toolpaths.")
         )
         )
 
 
-        grid2.addWidget(self.stdlabel, 6, 0)
-        grid2.addWidget(self.slot_tooldia_entry, 6, 1)
-        grid2.addWidget(self.generate_milling_slots_button, 6, 2)
+        grid2.addWidget(self.stdlabel, 5, 0)
+        grid2.addWidget(self.slot_tooldia_entry, 5, 1)
+        grid2.addWidget(self.generate_milling_slots_button, 5, 2)
 
 
     def hide_drills(self, state=True):
     def hide_drills(self, state=True):
         if state is True:
         if state is True:
@@ -1030,10 +1074,10 @@ class GeometryObjectUI(ObjectUI):
     User interface for Geometry objects.
     User interface for Geometry objects.
     """
     """
 
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         super(GeometryObjectUI, self).__init__(title=_('Geometry Object'),
         super(GeometryObjectUI, self).__init__(title=_('Geometry Object'),
-                                               icon_file='share/geometry32.png', parent=parent)
-        self.decimals = 4
+                                               icon_file='share/geometry32.png', parent=parent, decimals=decimals)
+        self.decimals = decimals
 
 
         # Plot options
         # Plot options
         self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
@@ -1152,6 +1196,8 @@ class GeometryObjectUI(ObjectUI):
         # Tool Offset
         # Tool Offset
         self.grid1 = QtWidgets.QGridLayout()
         self.grid1 = QtWidgets.QGridLayout()
         self.geo_tools_box.addLayout(self.grid1)
         self.geo_tools_box.addLayout(self.grid1)
+        self.grid1.setColumnStretch(0, 0)
+        self.grid1.setColumnStretch(1, 1)
 
 
         self.tool_offset_lbl = QtWidgets.QLabel('%s:' % _('Tool Offset'))
         self.tool_offset_lbl = QtWidgets.QLabel('%s:' % _('Tool Offset'))
         self.tool_offset_lbl.setToolTip(
         self.tool_offset_lbl.setToolTip(
@@ -1162,70 +1208,57 @@ class GeometryObjectUI(ObjectUI):
                 "cut and negative for 'inside' cut."
                 "cut and negative for 'inside' cut."
             )
             )
         )
         )
-        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
         self.tool_offset_entry = FCDoubleSpinner()
         self.tool_offset_entry = FCDoubleSpinner()
         self.tool_offset_entry.set_precision(self.decimals)
         self.tool_offset_entry.set_precision(self.decimals)
         self.tool_offset_entry.setRange(-9999.9999, 9999.9999)
         self.tool_offset_entry.setRange(-9999.9999, 9999.9999)
         self.tool_offset_entry.setSingleStep(0.1)
         self.tool_offset_entry.setSingleStep(0.1)
 
 
-        spacer_lbl = QtWidgets.QLabel(" ")
-        spacer_lbl.setMinimumWidth(80)
-
-        self.grid1.addWidget(self.tool_offset_entry, 0, 1)
-        self.grid1.addWidget(spacer_lbl, 0, 2)
-
-        # ### Add a new Tool ####
-        hlay = QtWidgets.QHBoxLayout()
-        self.geo_tools_box.addLayout(hlay)
+        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
+        self.grid1.addWidget(self.tool_offset_entry, 0, 1, 1, 2)
 
 
-        # self.addtool_label = QtWidgets.QLabel('<b>Tool</b>')
-        # self.addtool_label.setToolTip(
-        #     "Add/Copy/Delete a tool to the tool list."
-        # )
         self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
-            _(
-                "Diameter for the new tool"
-            )
+            _("Diameter for the new tool")
         )
         )
         self.addtool_entry = FCDoubleSpinner()
         self.addtool_entry = FCDoubleSpinner()
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.setRange(0.00001, 9999.9999)
         self.addtool_entry.setRange(0.00001, 9999.9999)
         self.addtool_entry.setSingleStep(0.1)
         self.addtool_entry.setSingleStep(0.1)
 
 
-        hlay.addWidget(self.addtool_entry_lbl)
-        hlay.addWidget(self.addtool_entry)
-
-        grid2 = QtWidgets.QGridLayout()
-        self.geo_tools_box.addLayout(grid2)
-
         self.addtool_btn = QtWidgets.QPushButton(_('Add'))
         self.addtool_btn = QtWidgets.QPushButton(_('Add'))
         self.addtool_btn.setToolTip(
         self.addtool_btn.setToolTip(
-            _(
-                "Add a new tool to the Tool Table\n"
-                "with the diameter specified above."
-            )
+            _("Add a new tool to the Tool Table\n"
+              "with the specified diameter.")
+        )
+
+        self.grid1.addWidget(self.addtool_entry_lbl, 1, 0)
+        self.grid1.addWidget(self.addtool_entry, 1, 1)
+        self.grid1.addWidget(self.addtool_btn, 1, 2)
+
+        self.addtool_from_db_btn = QtWidgets.QPushButton(_('Add Tool from DataBase'))
+        self.addtool_from_db_btn.setToolTip(
+            _("Add a new tool to the Tool Table\n"
+              "from the Tool DataBase.")
         )
         )
+        self.grid1.addWidget(self.addtool_from_db_btn, 2, 0, 1, 3)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.geo_tools_box.addLayout(grid2)
 
 
         self.copytool_btn = QtWidgets.QPushButton(_('Copy'))
         self.copytool_btn = QtWidgets.QPushButton(_('Copy'))
         self.copytool_btn.setToolTip(
         self.copytool_btn.setToolTip(
-            _(
-                "Copy a selection of tools in the Tool Table\n"
-                "by first selecting a row in the Tool Table."
-            )
+            _("Copy a selection of tools in the Tool Table\n"
+              "by first selecting a row in the Tool Table.")
         )
         )
 
 
         self.deltool_btn = QtWidgets.QPushButton(_('Delete'))
         self.deltool_btn = QtWidgets.QPushButton(_('Delete'))
         self.deltool_btn.setToolTip(
         self.deltool_btn.setToolTip(
-            _(
-                "Delete a selection of tools in the Tool Table\n"
-                "by first selecting a row in the Tool Table."
-            )
+            _("Delete a selection of tools in the Tool Table\n"
+              "by first selecting a row in the Tool Table.")
         )
         )
 
 
-        grid2.addWidget(self.addtool_btn, 0, 0)
-        grid2.addWidget(self.copytool_btn, 0, 1)
-        grid2.addWidget(self.deltool_btn, 0, 2)
+        grid2.addWidget(self.copytool_btn, 0, 0)
+        grid2.addWidget(self.deltool_btn, 0, 1)
 
 
         self.empty_label = QtWidgets.QLabel('')
         self.empty_label = QtWidgets.QLabel('')
         self.geo_tools_box.addWidget(self.empty_label)
         self.geo_tools_box.addWidget(self.empty_label)
@@ -1233,8 +1266,15 @@ class GeometryObjectUI(ObjectUI):
         # ##################
         # ##################
         # Create CNC Job ###
         # Create CNC Job ###
         # ##################
         # ##################
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.geo_tools_box.addWidget(separator_line)
+
         # ### Tools Data ## ##
         # ### Tools Data ## ##
-        self.tool_data_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Data'))
+        self.tool_data_label = QtWidgets.QLabel(
+            "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), int(1)))
         self.tool_data_label.setToolTip(
         self.tool_data_label.setToolTip(
             _(
             _(
                 "The data used for creating GCode.\n"
                 "The data used for creating GCode.\n"
@@ -1295,7 +1335,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_precision(self.decimals)
-        self.cutz_entry.setRange(-9999.9999, -0.00001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-9999.9999, -0.00001)
+        else:
+            self.cutz_entry.setRange(-9999.9999, 9999.9999)
+
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(cutzlabel, 3, 0)
         self.grid3.addWidget(cutzlabel, 3, 0)
@@ -1335,7 +1380,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry.set_precision(self.decimals)
         self.travelz_entry.set_precision(self.decimals)
-        self.travelz_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.travelz_entry.setRange(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.setRange(-9999.9999, 9999.9999)
+
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(travelzlabel, 5, 0)
         self.grid3.addWidget(travelzlabel, 5, 0)
@@ -1358,7 +1408,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry.set_precision(self.decimals)
         self.toolchangez_entry.set_precision(self.decimals)
-        self.toolchangez_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.setRange(0, 9999.9999)
+        else:
+            self.toolchangez_entry.setRange(-9999.9999, 9999.9999)
+
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(self.toolchangeg_cb, 6, 0, 1, 2)
         self.grid3.addWidget(self.toolchangeg_cb, 6, 0, 1, 2)
@@ -1385,14 +1440,19 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.gendz_entry = FCDoubleSpinner()
         self.gendz_entry = FCDoubleSpinner()
         self.gendz_entry.set_precision(self.decimals)
         self.gendz_entry.set_precision(self.decimals)
-        self.gendz_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.gendz_entry.setRange(0, 9999.9999)
+        else:
+            self.gendz_entry.setRange(-9999.9999, 9999.9999)
+
         self.gendz_entry.setSingleStep(0.1)
         self.gendz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(self.endzlabel, 9, 0)
         self.grid3.addWidget(self.endzlabel, 9, 0)
         self.grid3.addWidget(self.gendz_entry, 9, 1)
         self.grid3.addWidget(self.gendz_entry, 9, 1)
 
 
         # Feedrate X-Y
         # Feedrate X-Y
-        frlabel = QtWidgets.QLabel('%s:' % _('Feed Rate X-Y'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate X-Y'))
         frlabel.setToolTip(
         frlabel.setToolTip(
             _("Cutting speed in the XY\n"
             _("Cutting speed in the XY\n"
               "plane in units per minute")
               "plane in units per minute")
@@ -1406,7 +1466,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
 
 
         # Feedrate Z (Plunge)
         # Feedrate Z (Plunge)
-        frzlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Z'))
+        frzlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
         frzlabel.setToolTip(
         frzlabel.setToolTip(
             _("Cutting speed in the XY\n"
             _("Cutting speed in the XY\n"
               "plane in units per minute.\n"
               "plane in units per minute.\n"
@@ -1421,7 +1481,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
 
 
         # Feedrate rapids
         # Feedrate rapids
-        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Rapids'))
+        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
         self.fr_rapidlabel.setToolTip(
         self.fr_rapidlabel.setToolTip(
             _("Cutting speed in the XY plane\n"
             _("Cutting speed in the XY plane\n"
               "(in units per minute).\n"
               "(in units per minute).\n"
@@ -1455,7 +1515,7 @@ class GeometryObjectUI(ObjectUI):
         spdlabel.setToolTip(
         spdlabel.setToolTip(
             _(
             _(
                 "Speed of the spindle in RPM (optional).\n"
                 "Speed of the spindle in RPM (optional).\n"
-                "If LASER postprocessor is used,\n"
+                "If LASER preprocessor is used,\n"
                 "this value is the power of laser."
                 "this value is the power of laser."
             )
             )
         )
         )
@@ -1485,10 +1545,10 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.dwell_cb, 15, 0)
         self.grid3.addWidget(self.dwell_cb, 15, 0)
         self.grid3.addWidget(self.dwelltime_entry, 15, 1)
         self.grid3.addWidget(self.dwelltime_entry, 15, 1)
 
 
-        # postprocessor selection
+        # preprocessor selection
         pp_label = QtWidgets.QLabel('%s:' % _("PostProcessor"))
         pp_label = QtWidgets.QLabel('%s:' % _("PostProcessor"))
         pp_label.setToolTip(
         pp_label.setToolTip(
-            _("The Postprocessor file that dictates\n"
+            _("The Preprocessor file that dictates\n"
               "the Machine Code (like GCode, RML, HPGL) output.")
               "the Machine Code (like GCode, RML, HPGL) output.")
         )
         )
         self.pp_geometry_name_cb = FCComboBox()
         self.pp_geometry_name_cb = FCComboBox()
@@ -1530,16 +1590,30 @@ class GeometryObjectUI(ObjectUI):
         self.feedrate_probe_label.hide()
         self.feedrate_probe_label.hide()
         self.feedrate_probe_entry.setVisible(False)
         self.feedrate_probe_entry.setVisible(False)
 
 
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line2, 19, 0, 1, 2)
+
+        self.apply_param_to_all = FCButton(_("Apply parameters to all tools"))
+        self.apply_param_to_all.setToolTip(
+            _("The parameters in the current form will be applied\n"
+              "on all the tools from the Tool Table.")
+        )
+        self.grid3.addWidget(self.apply_param_to_all, 20, 0, 1, 2)
+
+        self.grid3.addWidget(QtWidgets.QLabel(''), 21, 0, 1, 2)
+
         warning_lbl = QtWidgets.QLabel(
         warning_lbl = QtWidgets.QLabel(
             _(
             _(
                 "Add at least one tool in the tool-table.\n"
                 "Add at least one tool in the tool-table.\n"
                 "Click the header to select all, or Ctrl + LMB\n"
                 "Click the header to select all, or Ctrl + LMB\n"
                 "for custom selection of tools."
                 "for custom selection of tools."
             ))
             ))
-        self.grid3.addWidget(warning_lbl, 19, 0, 1, 2)
+        self.grid3.addWidget(warning_lbl, 22, 0, 1, 2)
 
 
         # Button
         # Button
-        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate'))
+        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate CNCJob object'))
         self.generate_cnc_button.setToolTip(
         self.generate_cnc_button.setToolTip(
             _("Generate the CNC Job object.")
             _("Generate the CNC Job object.")
         )
         )
@@ -1572,14 +1646,15 @@ class CNCObjectUI(ObjectUI):
     User interface for CNCJob objects.
     User interface for CNCJob objects.
     """
     """
 
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         """
         """
         Creates the user interface for CNCJob objects. GUI elements should
         Creates the user interface for CNCJob objects. GUI elements should
         be placed in ``self.custom_box`` to preserve the layout.
         be placed in ``self.custom_box`` to preserve the layout.
         """
         """
-
-        ObjectUI.__init__(self, title=_('CNC Job Object'), icon_file='share/cnc32.png', parent=parent)
-        self.decimals = 4
+        
+        ObjectUI.__init__(self, title=_('CNC Job Object'), icon_file='share/cnc32.png', parent=parent, 
+                          decimals=decimals)
+        self.decimals = decimals
 
 
         for i in range(0, self.common_grid.count()):
         for i in range(0, self.common_grid.count()):
             self.common_grid.itemAt(i).widget().hide()
             self.common_grid.itemAt(i).widget().hide()
@@ -1744,6 +1819,10 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(prependlabel)
         self.custom_box.addWidget(prependlabel)
 
 
         self.prepend_text = FCTextArea()
         self.prepend_text = FCTextArea()
+        self.prepend_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to add at the beginning of the G-Code file.")
+        )
         self.custom_box.addWidget(self.prepend_text)
         self.custom_box.addWidget(self.prepend_text)
 
 
         # Append text to GCode
         # Append text to GCode
@@ -1756,6 +1835,11 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(appendlabel)
         self.custom_box.addWidget(appendlabel)
 
 
         self.append_text = FCTextArea()
         self.append_text = FCTextArea()
+        self.append_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to append to the generated file. "
+              "I.e.: M2 (End of program)")
+        )
         self.custom_box.addWidget(self.append_text)
         self.custom_box.addWidget(self.append_text)
 
 
         self.cnc_frame = QtWidgets.QFrame()
         self.cnc_frame = QtWidgets.QFrame()
@@ -1774,7 +1858,7 @@ class CNCObjectUI(ObjectUI):
                 "This will constitute a Custom Toolchange GCode,\n"
                 "This will constitute a Custom Toolchange GCode,\n"
                 "or a Toolchange Macro.\n"
                 "or a Toolchange Macro.\n"
                 "The FlatCAM variables are surrounded by '%' symbol.\n\n"
                 "The FlatCAM variables are surrounded by '%' symbol.\n\n"
-                "WARNING: it can be used only with a postprocessor file\n"
+                "WARNING: it can be used only with a preprocessor file\n"
                 "that has 'toolchange_custom' in it's name and this is built\n"
                 "that has 'toolchange_custom' in it's name and this is built\n"
                 "having as template the 'Toolchange Custom' posprocessor file."
                 "having as template the 'Toolchange Custom' posprocessor file."
             )
             )
@@ -1782,6 +1866,17 @@ class CNCObjectUI(ObjectUI):
         self.cnc_box.addWidget(self.toolchangelabel)
         self.cnc_box.addWidget(self.toolchangelabel)
 
 
         self.toolchange_text = FCTextArea()
         self.toolchange_text = FCTextArea()
+        self.toolchange_text.setPlaceholderText(
+            _(
+                "Type here any G-Code commands you would "
+                "like to be executed when Toolchange event is encountered. "
+                "This will constitute a Custom Toolchange GCode, "
+                "or a Toolchange Macro. "
+                "The FlatCAM variables are surrounded by '%' symbol. \n"
+                "WARNING: it can be used only with a preprocessor file "
+                "that has 'toolchange_custom' in it's name."
+            )
+        )
         self.cnc_box.addWidget(self.toolchange_text)
         self.cnc_box.addWidget(self.toolchange_text)
 
 
         cnclay = QtWidgets.QHBoxLayout()
         cnclay = QtWidgets.QHBoxLayout()
@@ -1861,7 +1956,7 @@ class ScriptObjectUI(ObjectUI):
     User interface for Script  objects.
     User interface for Script  objects.
     """
     """
 
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         """
         """
         Creates the user interface for Script objects. GUI elements should
         Creates the user interface for Script objects. GUI elements should
         be placed in ``self.custom_box`` to preserve the layout.
         be placed in ``self.custom_box`` to preserve the layout.
@@ -1870,7 +1965,10 @@ class ScriptObjectUI(ObjectUI):
         ObjectUI.__init__(self, title=_('Script Object'),
         ObjectUI.__init__(self, title=_('Script Object'),
                           icon_file='share/script_new24.png',
                           icon_file='share/script_new24.png',
                           parent=parent,
                           parent=parent,
-                          common=False)
+                          common=False,
+                          decimals=decimals)
+
+        self.decimals = decimals
 
 
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()
@@ -1913,7 +2011,7 @@ class DocumentObjectUI(ObjectUI):
     User interface for Notes objects.
     User interface for Notes objects.
     """
     """
 
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         """
         """
         Creates the user interface for Notes objects. GUI elements should
         Creates the user interface for Notes objects. GUI elements should
         be placed in ``self.custom_box`` to preserve the layout.
         be placed in ``self.custom_box`` to preserve the layout.
@@ -1922,7 +2020,10 @@ class DocumentObjectUI(ObjectUI):
         ObjectUI.__init__(self, title=_('Document Object'),
         ObjectUI.__init__(self, title=_('Document Object'),
                           icon_file='share/notes16_1.png',
                           icon_file='share/notes16_1.png',
                           parent=parent,
                           parent=parent,
-                          common=False)
+                          common=False,
+                          decimals=decimals)
+
+        self.decimals = decimals
 
 
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()

+ 114 - 70
flatcamGUI/PlotCanvas.py

@@ -57,7 +57,61 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
 
 
         # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
         # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
         # which might decrease performance
         # which might decrease performance
-        self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
+        # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
+        self.workspace_line = None
+
+        self.pagesize_dict = dict()
+        self.pagesize_dict.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5*25.4, 11*25.4),
+                'LEGAL': (8.5*25.4, 14*25.4),
+                'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*25.4, 8*25.4),
+                'HALF_LETTER': (5.5*25.4, 8*25.4),
+                'GOV_LETTER': (8*25.4, 10.5*25.4),
+                'GOV_LEGAL': (8.5*25.4, 13*25.4),
+                'LEDGER': (17*25.4, 11*25.4),
+            }
+        )
 
 
         # <VisPyCanvas>
         # <VisPyCanvas>
         self.create_native()
         self.create_native()
@@ -67,16 +121,16 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.container.addWidget(self.native)
         self.container.addWidget(self.native)
 
 
         # ## AXIS # ##
         # ## AXIS # ##
-        self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
+        self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
                                    parent=self.view.scene)
                                    parent=self.view.scene)
 
 
-        self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
+        self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
                                    parent=self.view.scene)
                                    parent=self.view.scene)
 
 
         # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
         # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
         # all CNC have a limited workspace
         # all CNC have a limited workspace
-
-        self.draw_workspace()
+        if self.fcapp.defaults['global_workspace'] is True:
+            self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
 
 
         self.line_parent = None
         self.line_parent = None
         self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
         self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
@@ -105,73 +159,43 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
 
 
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
 
 
-    # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
-    # all CNC have a limited workspace
-    def draw_workspace(self):
-        a = np.empty((0, 0))
-
-        a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
-        a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
-        a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
-        a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
-
-        a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
-        a4l_mm = np.array([(0, 0), (297, 0), (297, 210), (0, 210)])
-        a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
-        a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
-
-        if self.fcapp.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
-            if self.fcapp.defaults['global_workspaceT'] == 'A4P':
-                a = a4p_mm
-            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
-                a = a4l_mm
-            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
-                a = a3p_mm
-            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
-                a = a3l_mm
+    def draw_workspace(self, workspace_size):
+        """
+        Draw a rectangular shape on canvas to specify our valid workspace.
+        :param workspace_size: the workspace size; tuple
+        :return:
+        """
+        try:
+            if self.fcapp.defaults['units'].upper() == 'MM':
+                dims = self.pagesize_dict[workspace_size]
+            else:
+                dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
+        except Exception as e:
+            log.debug("PlotCanvas.draw_workspace() --> %s" % str(e))
+            return
+
+        if self.fcapp.defaults['global_workspace_orientation'] == 'l':
+            dims = (dims[1], dims[0])
+
+        a = np.array([(0, 0), (dims[0], 0), (dims[0], dims[1]), (0, dims[1])])
+
+        if not self.workspace_line:
+            self.workspace_line = Line(pos=np.array((a[0], a[1], a[2], a[3], a[0])), color=(0.70, 0.3, 0.3, 0.7),
+                                       antialias=True, method='agg', parent=self.view.scene)
         else:
         else:
-            if self.fcapp.defaults['global_workspaceT'] == 'A4P':
-                a = a4p_in
-            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
-                a = a4l_in
-            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
-                a = a3p_in
-            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
-                a = a3l_in
-
-        self.delete_workspace()
-
-        self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias=True, method='agg', parent=self.view.scene)
-        self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias=True, method='agg', parent=self.view.scene)
-
-        self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias=True, method='agg', parent=self.view.scene)
-        self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
-                           antialias=True, method='agg', parent=self.view.scene)
-
-        if self.fcapp.defaults['global_workspace'] is False:
-            self.delete_workspace()
-
-    # delete the workspace lines from the plot by removing the parent
+            self.workspace_line.parent = self.view.scene
+
     def delete_workspace(self):
     def delete_workspace(self):
         try:
         try:
-            self.b_line.parent = None
-            self.r_line.parent = None
-            self.t_line.parent = None
-            self.l_line.parent = None
-        except Exception as e:
+            self.workspace_line.parent = None
+        except Exception:
             pass
             pass
 
 
-    # redraw the workspace lines on the plot by readding them to the parent view.scene
+    # redraw the workspace lines on the plot by re adding them to the parent view.scene
     def restore_workspace(self):
     def restore_workspace(self):
         try:
         try:
-            self.b_line.parent = self.view.scene
-            self.r_line.parent = self.view.scene
-            self.t_line.parent = self.view.scene
-            self.l_line.parent = self.view.scene
-        except Exception as e:
+            self.workspace_line.parent = self.view.scene
+        except Exception:
             pass
             pass
 
 
     def graph_event_connect(self, event_name, callback):
     def graph_event_connect(self, event_name, callback):
@@ -310,12 +334,32 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
             except TypeError:
             except TypeError:
                 pass
                 pass
 
 
-        # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
+        # adjust the view camera to be slightly bigger than the bounds so the shape collection can be seen clearly
         # otherwise the shape collection boundary will have no border
         # otherwise the shape collection boundary will have no border
-        rect.left *= 0.96
-        rect.bottom *= 0.96
-        rect.right *= 1.01
-        rect.top *= 1.01
+        dx = rect.right - rect.left
+        dy = rect.top - rect.bottom
+        x_factor = dx * 0.02
+        y_factor = dy * 0.02
+
+        rect.left -= x_factor
+        rect.bottom -= y_factor
+        rect.right += x_factor
+        rect.top += y_factor
+
+        # rect.left *= 0.96
+        # rect.bottom *= 0.96
+        # rect.right *= 1.04
+        # rect.top *= 1.04
+
+        # units = self.fcapp.defaults['units'].upper()
+        # if units == 'MM':
+        #     compensation = 0.5
+        # else:
+        #     compensation = 0.5 / 25.4
+        # rect.left -= compensation
+        # rect.bottom -= compensation
+        # rect.right += compensation
+        # rect.top += compensation
 
 
         self.view.camera.rect = rect
         self.view.camera.rect = rect
 
 

+ 113 - 4
flatcamGUI/PlotCanvasLegacy.py

@@ -30,6 +30,7 @@ from matplotlib import use as mpl_use
 mpl_use("Qt5Agg")
 mpl_use("Qt5Agg")
 from matplotlib.figure import Figure
 from matplotlib.figure import Figure
 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.lines import Line2D
 # from matplotlib.widgets import Cursor
 # from matplotlib.widgets import Cursor
 
 
 fcTranslate.apply_language('strings')
 fcTranslate.apply_language('strings')
@@ -152,6 +153,64 @@ class PlotCanvasLegacy(QtCore.QObject):
             theme_color = '#000000'
             theme_color = '#000000'
             tick_color = '#FFFFFF'
             tick_color = '#FFFFFF'
 
 
+        # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
+        # which might decrease performance
+        # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
+        self.workspace_line = None
+
+        self.pagesize_dict = dict()
+        self.pagesize_dict.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5*25.4, 11*25.4),
+                'LEGAL': (8.5*25.4, 14*25.4),
+                'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*25.4, 8*25.4),
+                'HALF_LETTER': (5.5*25.4, 8*25.4),
+                'GOV_LETTER': (8*25.4, 10.5*25.4),
+                'GOV_LEGAL': (8.5*25.4, 13*25.4),
+                'LEDGER': (17*25.4, 11*25.4),
+            }
+        )
+
         # Options
         # Options
         self.x_margin = 15  # pixels
         self.x_margin = 15  # pixels
         self.y_margin = 25  # Pixels
         self.y_margin = 25  # Pixels
@@ -169,8 +228,8 @@ class PlotCanvasLegacy(QtCore.QObject):
         self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
         self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
         self.axes.set_aspect(1)
         self.axes.set_aspect(1)
         self.axes.grid(True, color='gray')
         self.axes.grid(True, color='gray')
-        self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
-        self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
+        self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
+        self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
 
 
         self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
         self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
         self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
         self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
@@ -240,6 +299,44 @@ class PlotCanvasLegacy(QtCore.QObject):
         # signal if there is a doubleclick
         # signal if there is a doubleclick
         self.is_dblclk = False
         self.is_dblclk = False
 
 
+        # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
+        # all CNC have a limited workspace
+        if self.app.defaults['global_workspace'] is True:
+            self.draw_workspace(workspace_size=self.app.defaults["global_workspaceT"])
+
+    def draw_workspace(self, workspace_size):
+        """
+        Draw a rectangular shape on canvas to specify our valid workspace.
+        :param workspace_size: the workspace size; tuple
+        :return:
+        """
+        try:
+            if self.app.defaults['units'].upper() == 'MM':
+                dims = self.pagesize_dict[workspace_size]
+            else:
+                dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
+        except Exception as e:
+            log.debug("PlotCanvasLegacy.draw_workspace() --> %s" % str(e))
+            return
+
+        if self.app.defaults['global_workspace_orientation'] == 'l':
+            dims = (dims[1], dims[0])
+
+        xdata = [0, dims[0], dims[0], 0, 0]
+        ydata = [0, 0, dims[1], dims[1], 0]
+
+        if self.workspace_line not in self.axes.lines:
+            self.workspace_line = Line2D(xdata=xdata, ydata=ydata, linewidth=2, antialiased=True, color='#b34d4d')
+            self.axes.add_line(self.workspace_line)
+            self.canvas.draw()
+
+    def delete_workspace(self):
+        try:
+            self.axes.lines.remove(self.workspace_line)
+            self.canvas.draw()
+        except Exception:
+            pass
+
     def graph_event_connect(self, event_name, callback):
     def graph_event_connect(self, event_name, callback):
         """
         """
         Attach an event handler to the canvas through the Matplotlib interface.
         Attach an event handler to the canvas through the Matplotlib interface.
@@ -272,8 +369,6 @@ class PlotCanvasLegacy(QtCore.QObject):
         :return: None
         :return: None
         """
         """
 
 
-        # self.double_click.disconnect(cid)
-
         self.canvas.mpl_disconnect(cid)
         self.canvas.mpl_disconnect(cid)
 
 
     def on_new_screen(self):
     def on_new_screen(self):
@@ -911,6 +1006,14 @@ class ShapeCollectionLegacy:
 
 
         return self.shape_id
         return self.shape_id
 
 
+    def remove(self, shape_id, update=None):
+        for k in list(self._shapes.keys()):
+            if shape_id == k:
+                self._shapes.pop(k, None)
+
+        if update is True:
+            self.redraw()
+
     def clear(self, update=None):
     def clear(self, update=None):
         """
         """
         Clear the canvas of the shapes.
         Clear the canvas of the shapes.
@@ -936,6 +1039,7 @@ class ShapeCollectionLegacy:
 
 
         :return: None
         :return: None
         """
         """
+
         path_num = 0
         path_num = 0
         local_shapes = deepcopy(self._shapes)
         local_shapes = deepcopy(self._shapes)
 
 
@@ -945,6 +1049,10 @@ class ShapeCollectionLegacy:
             obj_type = 'utility'
             obj_type = 'utility'
 
 
         if self._visible:
         if self._visible:
+            # if we don't use this then when adding each new shape, the old ones will be added again, too
+            if obj_type == 'utility':
+                self.axes.patches.clear()
+
             for element in local_shapes:
             for element in local_shapes:
                 if obj_type == 'excellon':
                 if obj_type == 'excellon':
                     # Plot excellon (All polygons?)
                     # Plot excellon (All polygons?)
@@ -1040,6 +1148,7 @@ class ShapeCollectionLegacy:
                                                  edgecolor=local_shapes[element]['color'],
                                                  edgecolor=local_shapes[element]['color'],
                                                  alpha=local_shapes[element]['alpha'],
                                                  alpha=local_shapes[element]['alpha'],
                                                  zorder=2)
                                                  zorder=2)
+
                             self.axes.add_patch(patch)
                             self.axes.add_patch(patch)
                         except Exception as e:
                         except Exception as e:
                             log.debug("ShapeCollectionLegacy.redraw() --> %s" % str(e))
                             log.debug("ShapeCollectionLegacy.redraw() --> %s" % str(e))

Plik diff jest za duży
+ 320 - 133
flatcamGUI/PreferencesUI.py


+ 7 - 4
flatcamGUI/VisPyPatches.py

@@ -117,18 +117,21 @@ def apply_patches():
                 minor.extend(np.linspace(maj + minstep,
                 minor.extend(np.linspace(maj + minstep,
                                          maj + majstep - minstep,
                                          maj + majstep - minstep,
                                          minor_num))
                                          minor_num))
+
             major_frac = (major - offset) / scale
             major_frac = (major - offset) / scale
-            minor_frac = (np.array(minor) - offset) / scale
             major_frac = major_frac[::-1] if flip else major_frac
             major_frac = major_frac[::-1] if flip else major_frac
             use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
             use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
             major_frac = major_frac[use_mask]
             major_frac = major_frac[use_mask]
             labels = [l for li, l in enumerate(labels) if use_mask[li]]
             labels = [l for li, l in enumerate(labels) if use_mask[li]]
-            minor_frac = minor_frac[(minor_frac > -0.0001) &
-                                    (minor_frac < 1.0001)]
+
+            minor_frac = (np.array(minor) - offset) / scale
+            use_minor_mask = (minor_frac > -0.0001) & (minor_frac < 1.0001)
+            minor_frac = minor_frac[use_minor_mask]
+
+            return major_frac, minor_frac, labels
         elif self.axis.scale_type == 'logarithmic':
         elif self.axis.scale_type == 'logarithmic':
             return NotImplementedError
             return NotImplementedError
         elif self.axis.scale_type == 'power':
         elif self.axis.scale_type == 'power':
             return NotImplementedError
             return NotImplementedError
-        return major_frac, minor_frac, labels
 
 
     Ticker._get_tick_frac_labels = _get_tick_frac_labels
     Ticker._get_tick_frac_labels = _get_tick_frac_labels

+ 4 - 2
flatcamParsers/ParseDXF.py

@@ -25,6 +25,7 @@ def dxfpoint2shapely(point):
     geo = Point(point.dxf.location).buffer(0.01)
     geo = Point(point.dxf.location).buffer(0.01)
     return geo
     return geo
 
 
+
 def dxfline2shapely(line):
 def dxfline2shapely(line):
 
 
     try:
     try:
@@ -39,6 +40,7 @@ def dxfline2shapely(line):
 
 
     return geo
     return geo
 
 
+
 def dxfcircle2shapely(circle, n_points=100):
 def dxfcircle2shapely(circle, n_points=100):
 
 
     ocs = circle.ocs()
     ocs = circle.ocs()
@@ -241,7 +243,7 @@ def dxfsolid2shapely(solid):
     try:
     try:
         corner_list.append(solid[iterator])
         corner_list.append(solid[iterator])
         iterator += 1
         iterator += 1
-    except:
+    except Exception:
         return Polygon(corner_list)
         return Polygon(corner_list)
 
 
 
 
@@ -265,7 +267,7 @@ def dxftrace2shapely(trace):
     try:
     try:
         corner_list.append(trace[iterator])
         corner_list.append(trace[iterator])
         iterator += 1
         iterator += 1
-    except:
+    except Exception:
         return Polygon(corner_list)
         return Polygon(corner_list)
 
 
 
 

+ 111 - 104
flatcamParsers/ParseExcellon.py

@@ -1,3 +1,10 @@
+# ########################################################## ##
+# FlatCAM: 2D Post-processing for Manufacturing               #
+# http://flatcam.org                                          #
+# Author: Juan Pablo Caram (c)                                #
+# Date: 2/5/2014                                              #
+# MIT Licence                                                 #
+# ########################################################## ##
 
 
 from camlib import Geometry
 from camlib import Geometry
 import FlatCAMApp
 import FlatCAMApp
@@ -9,6 +16,7 @@ import numpy as np
 import re
 import re
 import logging
 import logging
 import traceback
 import traceback
+from copy import deepcopy
 
 
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 
 
@@ -77,6 +85,7 @@ class Excellon(Geometry):
         :return: Excellon object.
         :return: Excellon object.
         :rtype: Excellon
         :rtype: Excellon
         """
         """
+        self.decimals = self.app.decimals
 
 
         if geo_steps_per_circle is None:
         if geo_steps_per_circle is None:
             geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
             geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
@@ -88,7 +97,6 @@ class Excellon(Geometry):
         self.tools = {}
         self.tools = {}
         # list to store the drills, see above for description
         # list to store the drills, see above for description
         self.drills = []
         self.drills = []
-
         # self.slots (list) to store the slots; each is a dictionary
         # self.slots (list) to store the slots; each is a dictionary
         self.slots = []
         self.slots = []
 
 
@@ -105,7 +113,7 @@ class Excellon(Geometry):
         self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
         self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
 
 
         # ## IN|MM -> Units are inherited from Geometry
         # ## IN|MM -> Units are inherited from Geometry
-        # self.units = units
+        self.units = self.app.defaults['units']
 
 
         # Trailing "T" or leading "L" (default)
         # Trailing "T" or leading "L" (default)
         # self.zeros = "T"
         # self.zeros = "T"
@@ -250,7 +258,7 @@ class Excellon(Geometry):
 
 
         try:
         try:
             self.parse_lines(estr)
             self.parse_lines(estr)
-        except:
+        except Exception:
             return "fail"
             return "fail"
 
 
     def parse_lines(self, elines):
     def parse_lines(self, elines):
@@ -303,8 +311,7 @@ class Excellon(Geometry):
                 # and we need to exit from here
                 # and we need to exit from here
                 if self.detect_gcode_re.search(eline):
                 if self.detect_gcode_re.search(eline):
                     log.warning("This is GCODE mark: %s" % eline)
                     log.warning("This is GCODE mark: %s" % eline)
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_('This is GCODE mark'), eline))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_('This is GCODE mark'), eline))
                     return
                     return
 
 
                 # Header Begin (M48) #
                 # Header Begin (M48) #
@@ -339,11 +346,11 @@ class Excellon(Geometry):
                             if line_units == 'MILS':
                             if line_units == 'MILS':
                                 spec = {"C": (float(match.group(2)) / 1000)}
                                 spec = {"C": (float(match.group(2)) / 1000)}
                                 self.tools[str(name_tool)] = spec
                                 self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                                log.debug("Tool definition: %s %s" % (name_tool, spec))
                             else:
                             else:
                                 spec = {"C": float(match.group(2))}
                                 spec = {"C": float(match.group(2))}
                                 self.tools[str(name_tool)] = spec
                                 self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                                log.debug("Tool definition: %s %s" % (name_tool, spec))
                             spec['solid_geometry'] = []
                             spec['solid_geometry'] = []
                             continue
                             continue
                     # search for Altium Excellon Format / Sprint Layout who is included as a comment
                     # search for Altium Excellon Format / Sprint Layout who is included as a comment
@@ -385,17 +392,18 @@ class Excellon(Geometry):
                 # object's units.
                 # object's units.
                 match = self.meas_re.match(eline)
                 match = self.meas_re.match(eline)
                 if match:
                 if match:
-                    # self.units = {"1": "MM", "2": "IN"}[match.group(1)]
+                    self.units = {"1": "MM", "2": "IN"}[match.group(1)]
 
 
                     # Modified for issue #80
                     # Modified for issue #80
-                    self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
-                    log.debug("  Units: %s" % self.units)
+                    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)
                     if self.units == 'MM':
                     if self.units == 'MM':
-                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \
-                                    ':' + str(self.excellon_format_lower_mm))
+                        log.warning("Excellon format preset is: %s:%s" %
+                                    (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
                     else:
                     else:
-                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \
-                                    ':' + str(self.excellon_format_lower_in))
+                        log.warning("Excellon format preset is: %s:%s" %
+                                    (str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
                     continue
                     continue
 
 
                 # ### Body ####
                 # ### Body ####
@@ -412,7 +420,7 @@ class Excellon(Geometry):
                                 name = str(int(match.group(1)))
                                 name = str(int(match.group(1)))
                                 try:
                                 try:
                                     diam = float(match.group(2))
                                     diam = float(match.group(2))
-                                except:
+                                except Exception:
                                     # it's possible that tool definition has only tool number and no diameter info
                                     # it's possible that tool definition has only tool number and no diameter info
                                     # (those could be in another file like PCB Wizard do)
                                     # (those could be in another file like PCB Wizard do)
                                     # then match.group(2) = None and float(None) will create the exception
                                     # then match.group(2) = None and float(None) will create the exception
@@ -476,7 +484,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_start_x
                                 slot_current_x = slot_start_x
                             except TypeError:
                             except TypeError:
                                 slot_start_x = slot_current_x
                                 slot_start_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -484,7 +492,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_start_y
                                 slot_current_y = slot_start_y
                             except TypeError:
                             except TypeError:
                                 slot_start_y = slot_current_y
                                 slot_start_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -492,7 +500,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_stop_x
                                 slot_current_x = slot_stop_x
                             except TypeError:
                             except TypeError:
                                 slot_stop_x = slot_current_x
                                 slot_stop_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -500,7 +508,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_stop_y
                                 slot_current_y = slot_stop_y
                             except TypeError:
                             except TypeError:
                                 slot_stop_y = slot_current_y
                                 slot_stop_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             if (slot_start_x is None or slot_start_y is None or
                             if (slot_start_x is None or slot_start_y is None or
@@ -546,7 +554,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_start_x
                                 slot_current_x = slot_start_x
                             except TypeError:
                             except TypeError:
                                 slot_start_x = slot_current_x
                                 slot_start_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -554,7 +562,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_start_y
                                 slot_current_y = slot_start_y
                             except TypeError:
                             except TypeError:
                                 slot_start_y = slot_current_y
                                 slot_start_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -562,7 +570,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_stop_x
                                 slot_current_x = slot_stop_x
                             except TypeError:
                             except TypeError:
                                 slot_stop_x = slot_current_x
                                 slot_stop_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             try:
                             try:
@@ -570,7 +578,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_stop_y
                                 slot_current_y = slot_stop_y
                             except TypeError:
                             except TypeError:
                                 slot_stop_y = slot_current_y
                                 slot_stop_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
                                 return
 
 
                             if (slot_start_x is None or slot_start_y is None or
                             if (slot_start_x is None or slot_start_y is None or
@@ -619,7 +627,7 @@ class Excellon(Geometry):
                         except TypeError:
                         except TypeError:
                             x = current_x
                             x = current_x
                             repeating_x = 0
                             repeating_x = 0
-                        except:
+                        except Exception:
                             return
                             return
 
 
                         try:
                         try:
@@ -629,7 +637,7 @@ class Excellon(Geometry):
                         except TypeError:
                         except TypeError:
                             y = current_y
                             y = current_y
                             repeating_y = 0
                             repeating_y = 0
-                        except:
+                        except Exception:
                             return
                             return
 
 
                         if x is None or y is None:
                         if x is None or y is None:
@@ -776,13 +784,13 @@ class Excellon(Geometry):
                         name = str(int(match.group(1)))
                         name = str(int(match.group(1)))
                         spec = {"C": float(match.group(2)), 'solid_geometry': []}
                         spec = {"C": float(match.group(2)), 'solid_geometry': []}
                         self.tools[name] = spec
                         self.tools[name] = spec
-                        log.debug("  Tool definition: %s %s" % (name, spec))
+                        log.debug("Tool definition: %s %s" % (name, spec))
                         continue
                         continue
 
 
                     # ## Units and number format # ##
                     # ## Units and number format # ##
                     match = self.units_re.match(eline)
                     match = self.units_re.match(eline)
                     if match:
                     if match:
-                        self.units_found = match.group(1)
+                        self.units = self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
                         self.excellon_format = match.group(3)
                         self.excellon_format = match.group(3)
                         if self.excellon_format:
                         if self.excellon_format:
@@ -796,51 +804,49 @@ class Excellon(Geometry):
                                 self.excellon_format_lower_in = lower
                                 self.excellon_format_lower_in = lower
 
 
                         # Modified for issue #80
                         # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                        # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                        log.warning("Units: %s" % self.units)
+                        log.warning("UNITS found inline before conversion: %s" % self.units)
+                        self.convert_units(self.units)
+                        log.warning("UNITS found inline after conversion: %s" % self.units)
                         if self.units == 'MM':
                         if self.units == 'MM':
-                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                        ':' + str(self.excellon_format_lower_mm))
+                            log.warning("Excellon format preset is: %s:%s" %
+                                        (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
                         else:
                         else:
-                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                        ':' + str(self.excellon_format_lower_in))
-                        log.warning("Type of zeros found inline: %s" % self.zeros)
+                            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)
                         continue
                         continue
 
 
                     # Search for units type again it might be alone on the line
                     # Search for units type again it might be alone on the line
                     if "INCH" in eline:
                     if "INCH" in eline:
-                        line_units = "INCH"
+                        line_units = "IN"
                         # Modified for issue #80
                         # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
-                        log.warning("Type of UNITS found inline: %s" % line_units)
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                    ':' + str(self.excellon_format_lower_in))
-                        # TODO: not working
-                        # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        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)))
                         continue
                         continue
                     elif "METRIC" in eline:
                     elif "METRIC" in eline:
-                        line_units = "METRIC"
+                        line_units = "MM"
                         # Modified for issue #80
                         # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
-                        log.warning("Type of UNITS found inline: %s" % line_units)
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                    ':' + str(self.excellon_format_lower_mm))
-                        # TODO: not working
-                        # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        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)))
                         continue
                         continue
 
 
                     # Search for zeros type again because it might be alone on the line
                     # Search for zeros type again because it might be alone on the line
                     match = re.search(r'[LT]Z', eline)
                     match = re.search(r'[LT]Z', eline)
                     if match:
                     if match:
                         self.zeros = match.group()
                         self.zeros = match.group()
-                        log.warning("Type of zeros found: %s" % self.zeros)
+                        log.warning("Type of ZEROS found: %s" % self.zeros)
                         continue
                         continue
 
 
                 # ## Units and number format outside header# ##
                 # ## Units and number format outside header# ##
                 match = self.units_re.match(eline)
                 match = self.units_re.match(eline)
                 if match:
                 if match:
-                    self.units_found = match.group(1)
+                    self.units = self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
                     self.zeros = match.group(2)  # "T" or "L". Might be empty
                     self.zeros = match.group(2)  # "T" or "L". Might be empty
                     self.excellon_format = match.group(3)
                     self.excellon_format = match.group(3)
                     if self.excellon_format:
                     if self.excellon_format:
@@ -854,18 +860,17 @@ class Excellon(Geometry):
                             self.excellon_format_lower_in = lower
                             self.excellon_format_lower_in = lower
 
 
                     # Modified for issue #80
                     # Modified for issue #80
-                    self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                    # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                    log.warning("Units: %s" % self.units)
+                    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)
+
                     if self.units == 'MM':
                     if self.units == 'MM':
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                    ':' + str(self.excellon_format_lower_mm))
+                        log.warning("Excellon format preset is: %s:%s" %
+                                    (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
                     else:
                     else:
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                    ':' + str(self.excellon_format_lower_in))
-                    log.warning("Type of zeros found outside header, inline: %s" % self.zeros)
-
-                    log.warning("UNITS found outside header")
+                        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)
                     continue
                     continue
 
 
                 log.warning("Line ignored: %s" % eline)
                 log.warning("Line ignored: %s" % eline)
@@ -873,8 +878,9 @@ class Excellon(Geometry):
             # make sure that since we are in headerless mode, we convert the tools only after the file parsing
             # 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
             # is finished since the tools definitions are spread in the Excellon body. We use as units the value
             # from self.defaults['excellon_units']
             # from self.defaults['excellon_units']
+
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
-        except Exception as e:
+        except Exception:
             log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
             log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
             msg = '[ERROR_NOTCL] %s' % \
             msg = '[ERROR_NOTCL] %s' % \
                   _("An internal error has ocurred. See shell.\n")
                   _("An internal error has ocurred. See shell.\n")
@@ -948,6 +954,8 @@ class Excellon(Geometry):
 
 
         :return: None
         :return: None
         """
         """
+
+        log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry()")
         self.solid_geometry = []
         self.solid_geometry = []
         try:
         try:
             # clear the solid_geometry in self.tools
             # clear the solid_geometry in self.tools
@@ -964,7 +972,7 @@ class Excellon(Geometry):
                                          _("Excellon.create_geometry() -> a drill location was skipped "
                                          _("Excellon.create_geometry() -> a drill location was skipped "
                                            "due of not having a tool associated.\n"
                                            "due of not having a tool associated.\n"
                                            "Check the resulting GCode."))
                                            "Check the resulting GCode."))
-                    log.debug("Excellon.create_geometry() -> a drill location was skipped "
+                    log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry() -> a drill location was skipped "
                               "due of not having a tool associated")
                               "due of not having a tool associated")
                     continue
                     continue
                 tooldia = self.tools[drill['tool']]['C']
                 tooldia = self.tools[drill['tool']]['C']
@@ -983,37 +991,10 @@ class Excellon(Geometry):
                 self.tools[slot['tool']]['solid_geometry'].append(poly)
                 self.tools[slot['tool']]['solid_geometry'].append(poly)
 
 
         except Exception as e:
         except Exception as e:
-            log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e))
+            log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry() -> "
+                      "Excellon geometry creation failed due of ERROR: %s" % str(e))
             return "fail"
             return "fail"
 
 
-        # drill_geometry = {}
-        # slot_geometry = {}
-        #
-        # def insertIntoDataStruct(dia, drill_geo, aDict):
-        #     if not dia in aDict:
-        #         aDict[dia] = [drill_geo]
-        #     else:
-        #         aDict[dia].append(drill_geo)
-        #
-        # for tool in self.tools:
-        #     tooldia = self.tools[tool]['C']
-        #     for drill in self.drills:
-        #         if drill['tool'] == tool:
-        #             poly = drill['point'].buffer(tooldia / 2.0)
-        #             insertIntoDataStruct(tooldia, poly, drill_geometry)
-        #
-        # for tool in self.tools:
-        #     slot_tooldia = self.tools[tool]['C']
-        #     for slot in self.slots:
-        #         if slot['tool'] == tool:
-        #             start = slot['start']
-        #             stop = slot['stop']
-        #             lines_string = LineString([start, stop])
-        #             poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
-        #             insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
-        #
-        # self.solid_geometry = [drill_geometry, slot_geometry]
-
     def bounds(self):
     def bounds(self):
         """
         """
         Returns coordinates of rectangular bounds
         Returns coordinates of rectangular bounds
@@ -1022,9 +1003,10 @@ class Excellon(Geometry):
         # fixed issue of getting bounds only for one level lists of objects
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
         # now it can get bounds for nested lists of objects
 
 
-        log.debug("camlib.Excellon.bounds()")
-        if self.solid_geometry is None:
-            log.debug("solid_geometry is None")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.bounds()")
+
+        if self.solid_geometry is None or not self.tools:
+            log.debug("flatcamParsers.ParseExcellon.Excellon -> solid_geometry is None")
             return 0, 0, 0, 0
             return 0, 0, 0, 0
 
 
         def bounds_rec(obj):
         def bounds_rec(obj):
@@ -1065,7 +1047,7 @@ class Excellon(Geometry):
             maxx_list.append(maxx)
             maxx_list.append(maxx)
             maxy_list.append(maxy)
             maxy_list.append(maxy)
 
 
-        return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
+        return min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)
 
 
     def convert_units(self, units):
     def convert_units(self, units):
         """
         """
@@ -1082,16 +1064,29 @@ class Excellon(Geometry):
         :type str: IN or MM
         :type str: IN or MM
         :return:
         :return:
         """
         """
-        log.debug("camlib.Excellon.convert_units()")
 
 
-        factor = Geometry.convert_units(self, units)
+        # 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
+        log.debug("flatcamParsers.ParseExcellon.Excellon.convert_units() --> Factor: %s" % str(factor))
+
+        self.units = obj_units
+        self.scale(factor, factor)
+        self.file_units_factor = factor
 
 
         # Tools
         # Tools
         for tname in self.tools:
         for tname in self.tools:
             self.tools[tname]["C"] *= factor
             self.tools[tname]["C"] *= factor
 
 
         self.create_geometry()
         self.create_geometry()
-
         return factor
         return factor
 
 
     def scale(self, xfactor, yfactor=None, point=None):
     def scale(self, xfactor, yfactor=None, point=None):
@@ -1106,7 +1101,7 @@ class Excellon(Geometry):
         :return: None
         :return: None
         :rtype: NOne
         :rtype: NOne
         """
         """
-        log.debug("camlib.Excellon.scale()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.scale()")
 
 
         if yfactor is None:
         if yfactor is None:
             yfactor = xfactor
             yfactor = xfactor
@@ -1117,6 +1112,9 @@ class Excellon(Geometry):
         else:
         else:
             px, py = point
             px, py = point
 
 
+        if xfactor == 0 and yfactor == 0:
+            return
+
         def scale_geom(obj):
         def scale_geom(obj):
             if type(obj) is list:
             if type(obj) is list:
                 new_obj = []
                 new_obj = []
@@ -1169,10 +1167,13 @@ class Excellon(Geometry):
         :type vect: tuple
         :type vect: tuple
         :return: None
         :return: None
         """
         """
-        log.debug("camlib.Excellon.offset()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.offset()")
 
 
         dx, dy = vect
         dx, dy = vect
 
 
+        if dx == 0 and dy == 0:
+            return
+
         def offset_geom(obj):
         def offset_geom(obj):
             if type(obj) is list:
             if type(obj) is list:
                 new_obj = []
                 new_obj = []
@@ -1227,7 +1228,7 @@ class Excellon(Geometry):
         :type point: list
         :type point: list
         :return: None
         :return: None
         """
         """
-        log.debug("camlib.Excellon.mirror()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.mirror()")
 
 
         px, py = point
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -1294,7 +1295,7 @@ class Excellon(Geometry):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
-        log.debug("camlib.Excellon.skew()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.skew()")
 
 
         if angle_x is None:
         if angle_x is None:
             angle_x = 0.0
             angle_x = 0.0
@@ -1302,6 +1303,9 @@ class Excellon(Geometry):
         if angle_y is None:
         if angle_y is None:
             angle_y = 0.0
             angle_y = 0.0
 
 
+        if angle_x == 0 and angle_y == 0:
+            return
+
         def skew_geom(obj):
         def skew_geom(obj):
             if type(obj) is list:
             if type(obj) is list:
                 new_obj = []
                 new_obj = []
@@ -1378,7 +1382,10 @@ class Excellon(Geometry):
         :param point: tuple of coordinates (x, y)
         :param point: tuple of coordinates (x, y)
         :return:
         :return:
         """
         """
-        log.debug("camlib.Excellon.rotate()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.rotate()")
+
+        if angle == 0:
+            return
 
 
         def rotate_geom(obj, origin=None):
         def rotate_geom(obj, origin=None):
             if type(obj) is list:
             if type(obj) is list:

+ 111 - 18
flatcamParsers/ParseGerber.py

@@ -9,13 +9,15 @@ import traceback
 from copy import deepcopy
 from copy import deepcopy
 import sys
 import sys
 
 
-from shapely.ops import cascaded_union
+from shapely.ops import cascaded_union, unary_union
 from shapely.geometry import Polygon, MultiPolygon, LineString, Point
 from shapely.geometry import Polygon, MultiPolygon, LineString, Point
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 from shapely.geometry import box as shply_box
 from shapely.geometry import box as shply_box
 
 
-import FlatCAMTranslation as fcTranslate
+from lxml import etree as ET
+from flatcamParsers.ParseSVG import *
 
 
+import FlatCAMTranslation as fcTranslate
 import gettext
 import gettext
 import builtins
 import builtins
 
 
@@ -81,6 +83,7 @@ class Gerber(Geometry):
 
 
         # How to approximate a circle with lines.
         # How to approximate a circle with lines.
         self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
         self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
+        self.decimals = self.app.decimals
 
 
         # Initialize parent
         # Initialize parent
         Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
         Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
@@ -258,7 +261,7 @@ class Gerber(Geometry):
 
 
         try:  # Could be empty for aperture macros
         try:  # Could be empty for aperture macros
             paramList = apParameters.split('X')
             paramList = apParameters.split('X')
-        except:
+        except Exception:
             paramList = None
             paramList = None
 
 
         if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
         if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
@@ -784,7 +787,7 @@ class Gerber(Geometry):
                         self.apertures['0'] = {}
                         self.apertures['0'] = {}
                         self.apertures['0']['type'] = 'REG'
                         self.apertures['0']['type'] = 'REG'
                         self.apertures['0']['size'] = 0.0
                         self.apertures['0']['size'] = 0.0
-                        self.apertures['0']['geometry'] = []
+                        self.apertures['0']['geometry'] = list()
 
 
                     # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
                     # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
                     # geo to the poly_buffer otherwise we loose it
                     # geo to the poly_buffer otherwise we loose it
@@ -867,7 +870,7 @@ class Gerber(Geometry):
                     # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
                     # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
                     #     try:
                     #     try:
                     #         current_operation_code = int(match.group(4))
                     #         current_operation_code = int(match.group(4))
-                    #     except:
+                    #     except Exception:
                     #         pass  # A line with just * will match too.
                     #         pass  # A line with just * will match too.
                     #     continue
                     #     continue
                     # NOTE: Letting it continue allows it to react to the
                     # NOTE: Letting it continue allows it to react to the
@@ -1082,7 +1085,7 @@ class Gerber(Geometry):
                                             geo_dict['clear'] = geo_s
                                             geo_dict['clear'] = geo_s
                                         else:
                                         else:
                                             geo_dict['solid'] = geo_s
                                             geo_dict['solid'] = geo_s
-                                except:
+                                except Exception:
                                     if self.app.defaults['gerber_simplification']:
                                     if self.app.defaults['gerber_simplification']:
                                         poly_buffer.append(geo_s.simplify(s_tol))
                                         poly_buffer.append(geo_s.simplify(s_tol))
                                     else:
                                     else:
@@ -1434,7 +1437,7 @@ class Gerber(Geometry):
                 #     for poly in new_poly:
                 #     for poly in new_poly:
                 #         try:
                 #         try:
                 #             self.solid_geometry = self.solid_geometry.union(poly)
                 #             self.solid_geometry = self.solid_geometry.union(poly)
-                #         except:
+                #         except Exception:
                 #             pass
                 #             pass
             else:
             else:
                 self.solid_geometry = self.solid_geometry.difference(new_poly)
                 self.solid_geometry = self.solid_geometry.difference(new_poly)
@@ -1607,10 +1610,10 @@ class Gerber(Geometry):
         """
         """
         Converts the units of the object to ``units`` by scaling all
         Converts the units of the object to ``units`` by scaling all
         the geometry appropriately. This call ``scale()``. Don't call
         the geometry appropriately. This call ``scale()``. Don't call
-        it again in descendents.
+        it again in descendants.
 
 
-        :param units: "IN" or "MM"
-        :type units: str
+        :param obj_units: "IN" or "MM"
+        :type obj_units: str
         :return: Scaling factor resulting from unit change.
         :return: Scaling factor resulting from unit change.
         :rtype: float
         :rtype: float
         """
         """
@@ -1635,6 +1638,87 @@ class Gerber(Geometry):
         self.scale(factor, factor)
         self.scale(factor, factor)
         return factor
         return factor
 
 
+    def import_svg(self, filename, object_type='gerber', flip=True, units='MM'):
+        """
+        Imports shapes from an SVG file into the object's geometry.
+
+        :param filename: Path to the SVG file.
+        :type filename: str
+        :param object_type: parameter passed further along
+        :param flip: Flip the vertically.
+        :type flip: bool
+        :param units: FlatCAM units
+        :return: None
+        """
+
+        log.debug("flatcamParsers.ParseGerber.Gerber.import_svg()")
+
+        # Parse into list of shapely objects
+        svg_tree = ET.parse(filename)
+        svg_root = svg_tree.getroot()
+
+        # Change origin to bottom left
+        # h = float(svg_root.get('height'))
+        # w = float(svg_root.get('width'))
+        h = svgparselength(svg_root.get('height'))[0]  # TODO: No units support yet
+
+        geos = getsvggeo(svg_root, 'gerber')
+        if flip:
+            geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = list()
+
+        # if type(self.solid_geometry) == list:
+        #     if type(geos) == list:
+        #         self.solid_geometry += geos
+        #     else:
+        #         self.solid_geometry.append(geos)
+        # else:  # It's shapely geometry
+        #     self.solid_geometry = [self.solid_geometry, geos]
+
+        if type(geos) == list:
+            # HACK for importing QRCODE exported by FlatCAM
+            if len(geos) == 1:
+                geo_qrcode = list()
+                geo_qrcode.append(Polygon(geos[0].exterior))
+                for i_el in geos[0].interiors:
+                    geo_qrcode.append(Polygon(i_el).buffer(0))
+                for poly in geo_qrcode:
+                    geos.append(poly)
+
+            if type(self.solid_geometry) == list:
+                self.solid_geometry += geos
+            else:
+                geos.append(self.solid_geometry)
+                self.solid_geometry = geos
+        else:
+            if type(self.solid_geometry) == list:
+                self.solid_geometry.append(geos)
+            else:
+                self.solid_geometry = [self.solid_geometry, geos]
+
+        # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
+        self.solid_geometry = list(self.flatten_list(self.solid_geometry))
+
+        try:
+            __ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        if '0' not in self.apertures:
+            self.apertures['0'] = dict()
+            self.apertures['0']['type'] = 'REG'
+            self.apertures['0']['size'] = 0.0
+            self.apertures['0']['geometry'] = list()
+
+        for pol in self.solid_geometry:
+            new_el = dict()
+            new_el['solid'] = pol
+            new_el['follow'] = pol.exterior
+            self.apertures['0']['geometry'].append(deepcopy(new_el))
+
     def scale(self, xfactor, yfactor=None, point=None):
     def scale(self, xfactor, yfactor=None, point=None):
         """
         """
         Scales the objects' geometry on the XY plane by a given factor.
         Scales the objects' geometry on the XY plane by a given factor.
@@ -1661,7 +1745,7 @@ class Gerber(Geometry):
 
 
         try:
         try:
             xfactor = float(xfactor)
             xfactor = float(xfactor)
-        except:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Scale factor has to be a number: integer or float."))
                                  _("Scale factor has to be a number: integer or float."))
             return
             return
@@ -1671,11 +1755,14 @@ class Gerber(Geometry):
         else:
         else:
             try:
             try:
                 yfactor = float(yfactor)
                 yfactor = float(yfactor)
-            except:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("Scale factor has to be a number: integer or float."))
                                      _("Scale factor has to be a number: integer or float."))
                 return
                 return
 
 
+        if xfactor == 0 and yfactor == 0:
+            return
+
         if point is None:
         if point is None:
             px = 0
             px = 0
             py = 0
             py = 0
@@ -1685,8 +1772,7 @@ class Gerber(Geometry):
         # variables to display the percentage of work done
         # variables to display the percentage of work done
         self.geo_len = 0
         self.geo_len = 0
         try:
         try:
-            for __ in self.solid_geometry:
-                self.geo_len += 1
+            self.geo_len = len(self.solid_geometry)
         except TypeError:
         except TypeError:
             self.geo_len = 1
             self.geo_len = 1
 
 
@@ -1751,8 +1837,7 @@ class Gerber(Geometry):
             log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
             log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
             return 'fail'
             return 'fail'
 
 
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Scale done."))
+        self.app.inform.emit('[success] %s' % _("Gerber Scale done."))
         self.app.proc_container.new_text = ''
         self.app.proc_container.new_text = ''
 
 
         # ## solid_geometry ???
         # ## solid_geometry ???
@@ -1792,6 +1877,9 @@ class Gerber(Geometry):
                                    "Probable you entered only one value in the Offset field."))
                                    "Probable you entered only one value in the Offset field."))
             return
             return
 
 
+        if dx == 0 and dy == 0:
+            return
+
         # variables to display the percentage of work done
         # variables to display the percentage of work done
         self.geo_len = 0
         self.geo_len = 0
         try:
         try:
@@ -1944,11 +2032,13 @@ class Gerber(Geometry):
 
 
         px, py = point
         px, py = point
 
 
+        if angle_x == 0 and angle_y == 0:
+            return
+
         # variables to display the percentage of work done
         # variables to display the percentage of work done
         self.geo_len = 0
         self.geo_len = 0
         try:
         try:
-            for __ in self.solid_geometry:
-                self.geo_len += 1
+            self.geo_len = len(self.solid_geometry)
         except TypeError:
         except TypeError:
             self.geo_len = 1
             self.geo_len = 1
 
 
@@ -2005,6 +2095,9 @@ class Gerber(Geometry):
 
 
         px, py = point
         px, py = point
 
 
+        if angle == 0:
+            return
+
         # variables to display the percentage of work done
         # variables to display the percentage of work done
         self.geo_len = 0
         self.geo_len = 0
         try:
         try:

+ 13 - 6
flatcamParsers/ParseSVG.py

@@ -22,7 +22,7 @@
 # import xml.etree.ElementTree as ET
 # import xml.etree.ElementTree as ET
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
 from svg.path.path import Move
 from svg.path.path import Move
-from shapely.geometry import LineString
+from shapely.geometry import LineString, LinearRing, MultiLineString
 from shapely.affinity import skew, affine_transform, rotate
 from shapely.affinity import skew, affine_transform, rotate
 import numpy as np
 import numpy as np
 
 
@@ -71,7 +71,6 @@ def path2shapely(path, object_type, res=1.0):
     rings = []
     rings = []
 
 
     for component in path:
     for component in path:
-
         # Line
         # Line
         if isinstance(component, Line):
         if isinstance(component, Line):
             start = component.start
             start = component.start
@@ -123,17 +122,25 @@ def path2shapely(path, object_type, res=1.0):
 
 
     if points:
     if points:
         rings.append(points)
         rings.append(points)
+
+    rings = MultiLineString(rings)
     if len(rings) > 0:
     if len(rings) > 0:
-        if len(rings) == 1:
+        if len(rings) == 1 and not isinstance(rings, MultiLineString):
             # Polygons are closed and require more than 2 points
             # Polygons are closed and require more than 2 points
             if Point(rings[0][0]).almost_equals(Point(rings[0][-1])) and len(rings[0]) > 2:
             if Point(rings[0][0]).almost_equals(Point(rings[0][-1])) and len(rings[0]) > 2:
                 geo_element = Polygon(rings[0])
                 geo_element = Polygon(rings[0])
             else:
             else:
                 geo_element = LineString(rings[0])
                 geo_element = LineString(rings[0])
         else:
         else:
-            geo_element = Polygon(rings[0], rings[1:])
+            try:
+                geo_element = Polygon(rings[0], rings[1:])
+            except Exception:
+                coords = list()
+                for line in rings:
+                    coords.append(line.coords[0])
+                    coords.append(line.coords[1])
+                geo_element = Polygon(coords)
         geometry.append(geo_element)
         geometry.append(geo_element)
-
     return geometry
     return geometry
 
 
 
 
@@ -298,7 +305,7 @@ def getsvggeo(node, object_type, root=None):
         root = node
         root = node
 
 
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
-    geo = []
+    geo = list()
 
 
     # Recurse
     # Recurse
     if len(node) > 0:
     if len(node) > 0:

+ 18 - 5
flatcamTools/ToolCalculators.py

@@ -30,7 +30,7 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.app = app
         self.app = app
-        self.decimals = 6
+        self.decimals = self.app.decimals
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -94,6 +94,8 @@ class ToolCalculator(FlatCAMTool):
         self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
         self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
         self.tipDia_entry = FCDoubleSpinner()
         self.tipDia_entry = FCDoubleSpinner()
         self.tipDia_entry.set_precision(self.decimals)
         self.tipDia_entry.set_precision(self.decimals)
+        self.tipDia_entry.set_range(0.0, 9999.9999)
+        self.tipDia_entry.setSingleStep(0.1)
 
 
         # self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipDia_label.setToolTip(
         self.tipDia_label.setToolTip(
@@ -102,6 +104,8 @@ class ToolCalculator(FlatCAMTool):
         )
         )
         self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
         self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
         self.tipAngle_entry = FCSpinner()
         self.tipAngle_entry = FCSpinner()
+        self.tipAngle_entry.set_range(0,180)
+        self.tipAngle_entry.setSingleStep(5)
 
 
         # self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
@@ -109,7 +113,7 @@ class ToolCalculator(FlatCAMTool):
 
 
         self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
         self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
         self.cutDepth_entry = FCDoubleSpinner()
         self.cutDepth_entry = FCDoubleSpinner()
-        self.cutDepth_entry.setMinimum(-1e10)    # to allow negative numbers without actually adding a real limit
+        self.cutDepth_entry.set_range(-9999.9999, 9999.9999)
         self.cutDepth_entry.set_precision(self.decimals)
         self.cutDepth_entry.set_precision(self.decimals)
 
 
         # self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -163,6 +167,7 @@ class ToolCalculator(FlatCAMTool):
         self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
         self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
         self.pcblength_entry = FCDoubleSpinner()
         self.pcblength_entry = FCDoubleSpinner()
         self.pcblength_entry.set_precision(self.decimals)
         self.pcblength_entry.set_precision(self.decimals)
+        self.pcblength_entry.set_range(0.0, 9999.9999)
 
 
         # self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
         self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
@@ -170,6 +175,7 @@ class ToolCalculator(FlatCAMTool):
         self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
         self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
         self.pcbwidth_entry = FCDoubleSpinner()
         self.pcbwidth_entry = FCDoubleSpinner()
         self.pcbwidth_entry.set_precision(self.decimals)
         self.pcbwidth_entry.set_precision(self.decimals)
+        self.pcbwidth_entry.set_range(0.0, 9999.9999)
 
 
         # self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
         self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
@@ -177,6 +183,8 @@ class ToolCalculator(FlatCAMTool):
         self.cdensity_label = QtWidgets.QLabel('%s:' % _("Current Density"))
         self.cdensity_label = QtWidgets.QLabel('%s:' % _("Current Density"))
         self.cdensity_entry = FCDoubleSpinner()
         self.cdensity_entry = FCDoubleSpinner()
         self.cdensity_entry.set_precision(self.decimals)
         self.cdensity_entry.set_precision(self.decimals)
+        self.cdensity_entry.set_range(0.0, 9999.9999)
+        self.cdensity_entry.setSingleStep(0.1)
 
 
         # self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
@@ -185,6 +193,8 @@ class ToolCalculator(FlatCAMTool):
         self.growth_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
         self.growth_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
         self.growth_entry = FCDoubleSpinner()
         self.growth_entry = FCDoubleSpinner()
         self.growth_entry.set_precision(self.decimals)
         self.growth_entry.set_precision(self.decimals)
+        self.growth_entry.set_range(0.0, 9999.9999)
+        self.growth_entry.setSingleStep(0.01)
 
 
         # self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
         self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
@@ -195,6 +205,8 @@ class ToolCalculator(FlatCAMTool):
         self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
         self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
         self.cvalue_entry = FCDoubleSpinner()
         self.cvalue_entry = FCDoubleSpinner()
         self.cvalue_entry.set_precision(self.decimals)
         self.cvalue_entry.set_precision(self.decimals)
+        self.cvalue_entry.set_range(0.0, 9999.9999)
+        self.cvalue_entry.setSingleStep(0.1)
 
 
         # self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cvaluelabel.setToolTip(_('This is the current intensity value\n'
         self.cvaluelabel.setToolTip(_('This is the current intensity value\n'
@@ -204,6 +216,8 @@ class ToolCalculator(FlatCAMTool):
         self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
         self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
         self.time_entry = FCDoubleSpinner()
         self.time_entry = FCDoubleSpinner()
         self.time_entry.set_precision(self.decimals)
         self.time_entry.set_precision(self.decimals)
+        self.time_entry.set_range(0.0, 9999.9999)
+        self.time_entry.setSingleStep(0.1)
 
 
         # self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.timelabel.setToolTip(_('This is the calculated time required for the procedure.\n'
         self.timelabel.setToolTip(_('This is the calculated time required for the procedure.\n'
@@ -274,7 +288,7 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs)
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         # ## Initialize form
         # ## Initialize form
         self.mm_entry.set_value('%.*f' % (self.decimals, 0))
         self.mm_entry.set_value('%.*f' % (self.decimals, 0))
@@ -311,8 +325,7 @@ class ToolCalculator(FlatCAMTool):
 
 
         tip_diameter = float(self.tipDia_entry.get_value())
         tip_diameter = float(self.tipDia_entry.get_value())
 
 
-        half_tip_angle = float(self.tipAngle_entry.get_value())
-        half_tip_angle /= 2
+        half_tip_angle = float(self.tipAngle_entry.get_value()) / 2.0
 
 
         cut_depth = float(self.cutDepth_entry.get_value())
         cut_depth = float(self.cutDepth_entry.get_value())
         cut_depth = -cut_depth if cut_depth < 0 else cut_depth
         cut_depth = -cut_depth if cut_depth < 0 else cut_depth

+ 1171 - 0
flatcamTools/ToolCalibration.py

@@ -0,0 +1,1171 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 3/10/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import FCDoubleSpinner, EvalEntry, FCCheckBox, OptionalInputSection
+from flatcamGUI.GUIElements import FCTable, FCComboBox, RadioSet
+from flatcamEditors.FlatCAMTextEditor import TextEditor
+
+from shapely.geometry import Point
+from shapely.geometry.base import *
+
+import math
+from datetime import datetime
+import logging
+from copy import deepcopy
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class ToolCalibration(FlatCAMTool):
+
+    toolName = _("Calibration Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+
+        self.decimals = self.app.decimals
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+        grid_lay.setColumnStretch(2, 0)
+
+        step_1 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 1: Acquire Calibration Points"))
+        step_1.setToolTip(
+            _("Pick four points by clicking inside the drill holes.\n"
+              "Those four points should be in the four\n"
+              "(as much as possible) corners of the Excellon object.")
+        )
+        grid_lay.addWidget(step_1, 0, 0, 1, 3)
+
+        self.cal_source_lbl = QtWidgets.QLabel("<b>%s:</b>" % _("Source Type"))
+        self.cal_source_lbl.setToolTip(_("The source of calibration points.\n"
+                                         "It can be:\n"
+                                         "- Object -> click a hole geo for Excellon or a pad for Gerber\n"
+                                         "- Free -> click freely on canvas to acquire the calibration points"))
+        self.cal_source_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
+                                          {'label': _('Free'), 'value': 'free'}],
+                                         stretch=False)
+
+        grid_lay.addWidget(self.cal_source_lbl, 1, 0)
+        grid_lay.addWidget(self.cal_source_radio, 1, 1, 1, 2)
+
+        self.obj_type_label = QtWidgets.QLabel("%s:" % _("Object Type"))
+
+        self.obj_type_combo = FCComboBox()
+        self.obj_type_combo.addItem(_("Gerber"))
+        self.obj_type_combo.addItem(_("Excellon"))
+        self.obj_type_combo.setCurrentIndex(1)
+
+        grid_lay.addWidget(self.obj_type_label, 2, 0)
+        grid_lay.addWidget(self.obj_type_combo, 2, 1, 1, 2)
+
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(1)
+
+        self.object_label = QtWidgets.QLabel("%s:" % _("Source object selection"))
+        self.object_label.setToolTip(
+            _("FlatCAM Object to be used as a source for reference points.")
+        )
+
+        grid_lay.addWidget(self.object_label, 3, 0, 1, 3)
+        grid_lay.addWidget(self.object_combo, 4, 0, 1, 3)
+
+        self.points_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Calibration Points'))
+        self.points_table_label.setToolTip(
+            _("Contain the expected calibration points and the\n"
+              "ones measured.")
+        )
+        grid_lay.addWidget(self.points_table_label, 5, 0, 1, 3)
+
+        self.points_table = FCTable()
+        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        # self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        grid_lay.addWidget(self.points_table, 6, 0, 1, 3)
+
+        self.points_table.setColumnCount(4)
+        self.points_table.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Name"),
+                _("Target"),
+                _("Found Delta")
+            ]
+        )
+        self.points_table.setRowCount(8)
+        row = 0
+
+        # BOTTOM LEFT
+        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_1.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
+
+        self.bottom_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Left X'))
+        self.points_table.setCellWidget(row, 1, self.bottom_left_coordx_lbl)
+        self.bottom_left_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coordx_tgt)
+        self.bottom_left_coordx_tgt.setReadOnly(True)
+        self.bottom_left_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_left_coordx_found)
+        row += 1
+
+        self.bottom_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Left Y'))
+        self.points_table.setCellWidget(row, 1, self.bottom_left_coordy_lbl)
+        self.bottom_left_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coordy_tgt)
+        self.bottom_left_coordy_tgt.setReadOnly(True)
+        self.bottom_left_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_left_coordy_found)
+
+        self.bottom_left_coordx_found.set_value(_("Origin"))
+        self.bottom_left_coordy_found.set_value(_("Origin"))
+        self.bottom_left_coordx_found.setDisabled(True)
+        self.bottom_left_coordy_found.setDisabled(True)
+        row += 1
+
+        # BOTTOM RIGHT
+        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_2.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+
+        self.bottom_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Right X'))
+        self.points_table.setCellWidget(row, 1, self.bottom_right_coordx_lbl)
+        self.bottom_right_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_right_coordx_tgt)
+        self.bottom_right_coordx_tgt.setReadOnly(True)
+        self.bottom_right_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_right_coordx_found)
+
+        row += 1
+
+        self.bottom_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Right Y'))
+        self.points_table.setCellWidget(row, 1, self.bottom_right_coordy_lbl)
+        self.bottom_right_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_right_coordy_tgt)
+        self.bottom_right_coordy_tgt.setReadOnly(True)
+        self.bottom_right_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_right_coordy_found)
+        row += 1
+
+        # TOP LEFT
+        id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_3.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_3)  # Tool name/id
+
+        self.top_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Left X'))
+        self.points_table.setCellWidget(row, 1, self.top_left_coordx_lbl)
+        self.top_left_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_left_coordx_tgt)
+        self.top_left_coordx_tgt.setReadOnly(True)
+        self.top_left_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_left_coordx_found)
+        row += 1
+
+        self.top_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Left Y'))
+        self.points_table.setCellWidget(row, 1, self.top_left_coordy_lbl)
+        self.top_left_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_left_coordy_tgt)
+        self.top_left_coordy_tgt.setReadOnly(True)
+        self.top_left_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_left_coordy_found)
+        row += 1
+
+        # TOP RIGHT
+        id_item_4 = QtWidgets.QTableWidgetItem('%d' % 4)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_4.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_4)  # Tool name/id
+
+        self.top_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Right X'))
+        self.points_table.setCellWidget(row, 1, self.top_right_coordx_lbl)
+        self.top_right_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coordx_tgt)
+        self.top_right_coordx_tgt.setReadOnly(True)
+        self.top_right_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_right_coordx_found)
+        row += 1
+
+        self.top_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Right Y'))
+        self.points_table.setCellWidget(row, 1, self.top_right_coordy_lbl)
+        self.top_right_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coordy_tgt)
+        self.top_right_coordy_tgt.setReadOnly(True)
+        self.top_right_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_right_coordy_found)
+
+        vertical_header = self.points_table.verticalHeader()
+        vertical_header.hide()
+        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.points_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+
+        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        # for x in range(4):
+        #     self.points_table.resizeColumnToContents(x)
+        self.points_table.resizeColumnsToContents()
+        self.points_table.resizeRowsToContents()
+
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
+
+        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
+        self.points_table.setMaximumHeight(self.points_table.getHeight() + 3)
+
+        # ## Get Points Button
+        self.start_button = QtWidgets.QPushButton(_("Get Points"))
+        self.start_button.setToolTip(
+            _("Pick four points by clicking on canvas if the source choice\n"
+              "is 'free' or inside the object geometry if the source is 'object'.\n"
+              "Those four points should be in the four squares of\n"
+              "the object.")
+        )
+        self.start_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.start_button, 7, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 3)
+
+        grid_lay.addWidget(QtWidgets.QLabel(''), 9, 0)
+
+        # STEP 2 #
+        step_2 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 2: Verification GCode"))
+        step_2.setToolTip(
+            _("Generate GCode file to locate and align the PCB by using\n"
+              "the four points acquired above.")
+        )
+        grid_lay.addWidget(step_2, 10, 0, 1, 3)
+
+        self.gcode_title_label = QtWidgets.QLabel('<b>%s</b>' % _('GCode Parameters'))
+        self.gcode_title_label.setToolTip(
+            _("Parameters used when creating the GCode in this tool.")
+        )
+        grid_lay.addWidget(self.gcode_title_label, 11, 0, 1, 3)
+
+        # Travel Z entry
+        travelz_lbl = QtWidgets.QLabel('%s:' % _("Travel Z"))
+        travelz_lbl.setToolTip(
+            _("Height (Z) for travelling between the points.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner()
+        self.travelz_entry.set_range(-9999.9999, 9999.9999)
+        self.travelz_entry.set_precision(self.decimals)
+        self.travelz_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(travelz_lbl, 12, 0)
+        grid_lay.addWidget(self.travelz_entry, 12, 1, 1, 2)
+
+        # Verification Z entry
+        verz_lbl = QtWidgets.QLabel('%s:' % _("Verification Z"))
+        verz_lbl.setToolTip(
+            _("Height (Z) for checking the point.")
+        )
+
+        self.verz_entry = FCDoubleSpinner()
+        self.verz_entry.set_range(-9999.9999, 9999.9999)
+        self.verz_entry.set_precision(self.decimals)
+        self.verz_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(verz_lbl, 13, 0)
+        grid_lay.addWidget(self.verz_entry, 13, 1, 1, 2)
+
+        # Zero the Z of the verification tool
+        self.zeroz_cb = FCCheckBox('%s' % _("Zero Z tool"))
+        self.zeroz_cb.setToolTip(
+            _("Include a sequence to zero the height (Z)\n"
+              "of the verification tool.")
+        )
+
+        grid_lay.addWidget(self.zeroz_cb, 14, 0, 1, 3)
+
+        # Toochange Z entry
+        toolchangez_lbl = QtWidgets.QLabel('%s:' % _("Toolchange Z"))
+        toolchangez_lbl.setToolTip(
+            _("Height (Z) for mounting the verification probe.")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner()
+        self.toolchangez_entry.set_range(0.0000, 9999.9999)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(toolchangez_lbl, 15, 0)
+        grid_lay.addWidget(self.toolchangez_entry, 15, 1, 1, 2)
+
+        self.z_ois = OptionalInputSection(self.zeroz_cb, [toolchangez_lbl, self.toolchangez_entry])
+
+        # ## GCode Button
+        self.gcode_button = QtWidgets.QPushButton(_("Generate GCode"))
+        self.gcode_button.setToolTip(
+            _("Generate GCode file to locate and align the PCB by using\n"
+              "the four points acquired above.")
+        )
+        self.gcode_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.gcode_button, 16, 0, 1, 3)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 17, 0, 1, 3)
+
+        grid_lay.addWidget(QtWidgets.QLabel(''), 18, 0, 1, 3)
+
+        # STEP 3 #
+        step_3 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 3: Adjustments"))
+        step_3.setToolTip(
+            _("Calculate Scale and Skew factors based on the differences (delta)\n"
+              "found when checking the PCB pattern. The differences must be filled\n"
+              "in the fields Found (Delta).")
+        )
+        grid_lay.addWidget(step_3, 19, 0, 1, 3)
+
+        # ## Factors Button
+        self.generate_factors_button = QtWidgets.QPushButton(_("Calculate Factors"))
+        self.generate_factors_button.setToolTip(
+            _("Calculate Scale and Skew factors based on the differences (delta)\n"
+              "found when checking the PCB pattern. The differences must be filled\n"
+              "in the fields Found (Delta).")
+        )
+        self.generate_factors_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.generate_factors_button, 20, 0, 1, 3)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 21, 0, 1, 3)
+
+        grid_lay.addWidget(QtWidgets.QLabel(''), 22, 0, 1, 3)
+
+        # STEP 4 #
+        step_4 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 4: Adjusted GCode"))
+        step_4.setToolTip(
+            _("Generate verification GCode file adjusted with\n"
+              "the factors above.")
+        )
+        grid_lay.addWidget(step_4, 23, 0, 1, 3)
+
+        self.scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
+        self.scalex_label.setToolTip(
+            _("Factor for Scale action over X axis.")
+        )
+        self.scalex_entry = FCDoubleSpinner()
+        self.scalex_entry.set_range(0, 9999.9999)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.scalex_label, 24, 0)
+        grid_lay.addWidget(self.scalex_entry, 24, 1, 1, 2)
+
+        self.scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
+        self.scaley_label.setToolTip(
+            _("Factor for Scale action over Y axis.")
+        )
+        self.scaley_entry = FCDoubleSpinner()
+        self.scaley_entry.set_range(0, 9999.9999)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.scaley_label, 25, 0)
+        grid_lay.addWidget(self.scaley_entry, 25, 1, 1, 2)
+
+        self.scale_button = QtWidgets.QPushButton(_("Apply Scale Factors"))
+        self.scale_button.setToolTip(
+            _("Apply Scale factors on the calibration points.")
+        )
+        self.scale_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.scale_button, 26, 0, 1, 3)
+
+        self.skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
+        self.skewx_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewx_entry = FCDoubleSpinner()
+        self.skewx_entry.set_range(-360, 360)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.skewx_label, 27, 0)
+        grid_lay.addWidget(self.skewx_entry, 27, 1, 1, 2)
+
+        self.skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
+        self.skewy_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewy_entry = FCDoubleSpinner()
+        self.skewy_entry.set_range(-360, 360)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.skewy_label, 28, 0)
+        grid_lay.addWidget(self.skewy_entry, 28, 1, 1, 2)
+
+        self.skew_button = QtWidgets.QPushButton(_("Apply Skew Factors"))
+        self.skew_button.setToolTip(
+            _("Apply Skew factors on the calibration points.")
+        )
+        self.skew_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.skew_button, 29, 0, 1, 3)
+
+        # final_factors_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Final Factors"))
+        # final_factors_lbl.setToolTip(
+        #     _("Generate verification GCode file adjusted with\n"
+        #       "the factors above.")
+        # )
+        # grid_lay.addWidget(final_factors_lbl, 27, 0, 1, 3)
+        #
+        # self.fin_scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
+        # self.fin_scalex_label.setToolTip(
+        #     _("Final factor for Scale action over X axis.")
+        # )
+        # self.fin_scalex_entry = FCDoubleSpinner()
+        # self.fin_scalex_entry.set_range(0, 9999.9999)
+        # self.fin_scalex_entry.set_precision(self.decimals)
+        # self.fin_scalex_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_scalex_label, 28, 0)
+        # grid_lay.addWidget(self.fin_scalex_entry, 28, 1, 1, 2)
+        #
+        # self.fin_scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
+        # self.fin_scaley_label.setToolTip(
+        #     _("Final factor for Scale action over Y axis.")
+        # )
+        # self.fin_scaley_entry = FCDoubleSpinner()
+        # self.fin_scaley_entry.set_range(0, 9999.9999)
+        # self.fin_scaley_entry.set_precision(self.decimals)
+        # self.fin_scaley_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_scaley_label, 29, 0)
+        # grid_lay.addWidget(self.fin_scaley_entry, 29, 1, 1, 2)
+        #
+        # self.fin_skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
+        # self.fin_skewx_label.setToolTip(
+        #     _("Final value for angle for Skew action, in degrees.\n"
+        #       "Float number between -360 and 359.")
+        # )
+        # self.fin_skewx_entry = FCDoubleSpinner()
+        # self.fin_skewx_entry.set_range(-360, 360)
+        # self.fin_skewx_entry.set_precision(self.decimals)
+        # self.fin_skewx_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_skewx_label, 30, 0)
+        # grid_lay.addWidget(self.fin_skewx_entry, 30, 1, 1, 2)
+        #
+        # self.fin_skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
+        # self.fin_skewy_label.setToolTip(
+        #     _("Final value for angle for Skew action, in degrees.\n"
+        #       "Float number between -360 and 359.")
+        # )
+        # self.fin_skewy_entry = FCDoubleSpinner()
+        # self.fin_skewy_entry.set_range(-360, 360)
+        # self.fin_skewy_entry.set_precision(self.decimals)
+        # self.fin_skewy_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_skewy_label, 31, 0)
+        # grid_lay.addWidget(self.fin_skewy_entry, 31, 1, 1, 2)
+
+        # ## Adjusted GCode Button
+
+        self.adj_gcode_button = QtWidgets.QPushButton(_("Generate Adjusted GCode"))
+        self.adj_gcode_button.setToolTip(
+            _("Generate verification GCode file adjusted with\n"
+              "the factors above.")
+        )
+        self.adj_gcode_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.adj_gcode_button, 35, 0, 1, 3)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 36, 0, 1, 3)
+
+        grid_lay.addWidget(QtWidgets.QLabel(''), 37, 0, 1, 3)
+
+        # STEP 5 #
+        step_5 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 5: Calibrate FlatCAM Objects"))
+        step_5.setToolTip(
+            _("Adjust the FlatCAM objects\n"
+              "with the factors determined and verified above.")
+        )
+        grid_lay.addWidget(step_5, 38, 0, 1, 3)
+
+        self.adj_object_type_combo = QtWidgets.QComboBox()
+        self.adj_object_type_combo.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
+        self.adj_object_type_combo.setCurrentIndex(0)
+
+        self.adj_object_type_label = QtWidgets.QLabel("%s:" % _("Adjusted object type"))
+        self.adj_object_type_label.setToolTip(
+            _("Type of the FlatCAM Object to be adjusted.")
+        )
+
+        grid_lay.addWidget(self.adj_object_type_label, 39, 0, 1, 3)
+        grid_lay.addWidget(self.adj_object_type_combo, 40, 0, 1, 3)
+
+        self.adj_object_combo = FCComboBox()
+        self.adj_object_combo.setModel(self.app.collection)
+        self.adj_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.adj_object_combo.setCurrentIndex(0)
+
+        self.adj_object_label = QtWidgets.QLabel("%s:" % _("Adjusted object selection"))
+        self.adj_object_label.setToolTip(
+            _("The FlatCAM Object to be adjusted.")
+        )
+
+        grid_lay.addWidget(self.adj_object_label, 41, 0, 1, 3)
+        grid_lay.addWidget(self.adj_object_combo, 42, 0, 1, 3)
+
+        # ## Adjust Objects Button
+        self.cal_button = QtWidgets.QPushButton(_("Calibrate"))
+        self.cal_button.setToolTip(
+            _("Adjust (scale and/or skew) the objects\n"
+              "with the factors determined above.")
+        )
+        self.cal_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.cal_button, 43, 0, 1, 3)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line2, 44, 0, 1, 3)
+
+        grid_lay.addWidget(QtWidgets.QLabel(''), 45, 0, 1, 3)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        self.mr = None
+        self.units = ''
+
+        # here store 4 points to be used for calibration
+        self.click_points = list()
+
+        # store the status of the grid
+        self.grid_status_memory = None
+
+        self.target_obj = None
+
+        # if the mouse events are connected to a local method set this True
+        self.local_connected = False
+
+        # reference for the tab where to open and view the verification GCode
+        self.gcode_editor_tab = None
+
+        # calibrated object
+        self.cal_object = None
+
+        # ## Signals
+        self.start_button.clicked.connect(self.on_start_collect_points)
+        self.gcode_button.clicked.connect(self.generate_verification_gcode)
+        self.generate_factors_button.clicked.connect(self.calculate_factors)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+        self.cal_source_radio.activated_custom.connect(self.on_cal_source_radio)
+
+        self.obj_type_combo.currentIndexChanged.connect(self.on_obj_type_combo)
+        self.adj_object_type_combo.currentIndexChanged.connect(self.on_adj_obj_type_combo)
+
+        self.cal_button.clicked.connect(self.on_cal_button_click)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolCalibration()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            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, _("Calibrate Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+E', **kwargs)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units'].upper()
+
+        if self.local_connected is True:
+            self.disconnect_cal_events()
+
+        self.reset_calibration_points()
+
+        self.cal_source_radio.set_value(self.app.defaults['tools_cal_calsource'])
+        self.travelz_entry.set_value(self.app.defaults['tools_cal_travelz'])
+        self.verz_entry.set_value(self.app.defaults['tools_cal_verz'])
+        self.zeroz_cb.set_value(self.app.defaults['tools_cal_zeroz'])
+        self.toolchangez_entry.set_value(self.app.defaults['tools_cal_toolchangez'])
+
+        self.scalex_entry.set_value(1.0)
+        self.scaley_entry.set_value(1.0)
+        self.skewx_entry.set_value(0.0)
+        self.skewy_entry.set_value(0.0)
+
+        # calibrated object
+        self.cal_object = None
+
+        self.app.inform.emit('%s...' % _("Tool initialized"))
+
+    def on_obj_type_combo(self):
+        obj_type = self.obj_type_combo.currentIndex()
+        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(0)
+
+    def on_adj_obj_type_combo(self):
+        obj_type = self.adj_object_type_combo.currentIndex()
+        self.adj_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.adj_object_combo.setCurrentIndex(0)
+
+    def on_cal_source_radio(self, val):
+        if val == 'object':
+            self.obj_type_label.setDisabled(False)
+            self.obj_type_combo.setDisabled(False)
+            self.object_label.setDisabled(False)
+            self.object_combo.setDisabled(False)
+        else:
+            self.obj_type_label.setDisabled(True)
+            self.obj_type_combo.setDisabled(True)
+            self.object_label.setDisabled(True)
+            self.object_combo.setDisabled(True)
+
+    def on_start_collect_points(self):
+
+        if self.cal_source_radio.get_value() == 'object':
+            selection_index = self.object_combo.currentIndex()
+            model_index = self.app.collection.index(selection_index, 0, self.object_combo.rootModelIndex())
+            try:
+                self.target_obj = model_index.internalPointer().obj
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no source FlatCAM object selected..."))
+                return
+
+        # disengage the grid snapping since it will be hard to find the drills on grid
+        if self.app.ui.grid_snap_btn.isChecked():
+            self.grid_status_memory = True
+            self.app.ui.grid_snap_btn.trigger()
+        else:
+            self.grid_status_memory = False
+
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.canvas.graph_event_disconnect(self.app.mr)
+
+        self.local_connected = True
+
+        self.reset_calibration_points()
+
+        self.app.inform.emit(_("Get First calibration point. Bottom Left..."))
+
+    def on_mouse_click_release(self, event):
+        if event.button == 1:
+            if self.app.is_legacy is False:
+                event_pos = event.pos
+            else:
+                event_pos = (event.xdata, event.ydata)
+
+            pos_canvas = self.canvas.translate_coords(event_pos)
+            click_pt = Point([pos_canvas[0], pos_canvas[1]])
+
+            if self.cal_source_radio.get_value() == 'object':
+                if self.target_obj.kind.lower() == 'excellon':
+                    for tool, tool_dict in self.target_obj.tools.items():
+                        for geo in tool_dict['solid_geometry']:
+                            if click_pt.within(geo):
+                                center_pt = geo.centroid
+                                self.click_points.append(
+                                    (
+                                        float('%.*f' % (self.decimals, center_pt.x)),
+                                        float('%.*f' % (self.decimals, center_pt.y))
+                                    )
+                                )
+                                self.check_points()
+                else:
+                    for apid, apid_val in self.target_obj.apertures.items():
+                        for geo_el in apid_val['geometry']:
+                            if 'solid' in geo_el:
+                                if click_pt.within(geo_el['solid']):
+                                    if isinstance(geo_el['follow'], Point):
+                                        center_pt = geo_el['solid'].centroid
+                                        self.click_points.append(
+                                            (
+                                                float('%.*f' % (self.decimals, center_pt.x)),
+                                                float('%.*f' % (self.decimals, center_pt.y))
+                                            )
+                                        )
+                                        self.check_points()
+            else:
+                self.click_points.append(
+                    (
+                        float('%.*f' % (self.decimals, click_pt.x)),
+                        float('%.*f' % (self.decimals, click_pt.y))
+                    )
+                )
+                self.check_points()
+
+    def check_points(self):
+        if len(self.click_points) == 1:
+            self.bottom_left_coordx_tgt.set_value(self.click_points[0][0])
+            self.bottom_left_coordy_tgt.set_value(self.click_points[0][1])
+            self.app.inform.emit(_("Get Second calibration point. Bottom Right..."))
+        elif len(self.click_points) == 2:
+            self.bottom_right_coordx_tgt.set_value(self.click_points[1][0])
+            self.bottom_right_coordy_tgt.set_value(self.click_points[1][1])
+            self.app.inform.emit(_("Get Third calibration point. Top Left..."))
+        elif len(self.click_points) == 3:
+            self.top_left_coordx_tgt.set_value(self.click_points[2][0])
+            self.top_left_coordy_tgt.set_value(self.click_points[2][1])
+            self.app.inform.emit(_("Get Forth calibration point. Top Right..."))
+        elif len(self.click_points) == 4:
+            self.top_right_coordx_tgt.set_value(self.click_points[3][0])
+            self.top_right_coordy_tgt.set_value(self.click_points[3][1])
+            self.app.inform.emit('[success] %s' % _("Done. All four points have been acquired."))
+            self.disconnect_cal_events()
+
+    def reset_calibration_points(self):
+        self.click_points = list()
+
+        self.bottom_left_coordx_tgt.set_value('')
+        self.bottom_left_coordy_tgt.set_value('')
+
+        self.bottom_right_coordx_tgt.set_value('')
+        self.bottom_right_coordy_tgt.set_value('')
+
+        self.top_left_coordx_tgt.set_value('')
+        self.top_left_coordy_tgt.set_value('')
+
+        self.top_right_coordx_tgt.set_value('')
+        self.top_right_coordy_tgt.set_value('')
+
+    def gcode_header(self):
+        log.debug("ToolCalibration.gcode_header()")
+        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+        gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
+                (str(self.app.version), str(self.app.version_date)) + '\n'
+
+        gcode += '(Name: ' + _('Verification GCode for FlatCAM Calibrate Tool') + ')\n'
+
+        gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
+        gcode += '(Created on ' + time_str + ')\n' + '\n'
+        gcode += 'G20\n' if self.units.upper() == 'IN' else 'G21\n'
+        gcode += 'G90\n'
+        gcode += 'G17\n'
+        gcode += 'G94\n\n'
+        return gcode
+
+    def close_tab(self):
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Gcode Viewer"):
+                wdg = self.app.ui.plot_tab_area.widget(idx)
+                wdg.deleteLater()
+                self.app.ui.plot_tab_area.removeTab(idx)
+
+    def generate_verification_gcode(self):
+
+        travel_z = '%.*f' % (self.decimals, self.travelz_entry.get_value())
+        toolchange_z = '%.*f' % (self.decimals, self.toolchangez_entry.get_value())
+        verification_z = '%.*f' % (self.decimals, self.verz_entry.get_value())
+
+        if len(self.click_points) != 4:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Four points are needed for GCode generation."))
+            return 'fail'
+
+        gcode = self.gcode_header()
+        if self.zeroz_cb.get_value():
+            gcode += 'M5\n'
+            gcode += f'G00 Z{toolchange_z}\n'
+            gcode += 'M0\n'
+            gcode += 'G01 Z0\n'
+            gcode += 'M0\n'
+            gcode += f'G00 Z{toolchange_z}\n'
+            gcode += 'M0\n'
+
+        gcode += f'G00 Z{travel_z}\n'
+        gcode += f'G00 X{self.click_points[0][0]} Y{self.click_points[0][1]}\n'
+        gcode += f'G01 Z{verification_z}\n'
+        gcode += 'M0\n'
+
+        gcode += f'G00 Z{travel_z}\n'
+        gcode += f'G00 X{self.click_points[2][0]} Y{self.click_points[2][1]}\n'
+        gcode += f'G01 Z{verification_z}\n'
+        gcode += 'M0\n'
+
+        gcode += f'G00 Z{travel_z}\n'
+        gcode += f'G00 X{self.click_points[3][0]} Y{self.click_points[3][1]}\n'
+        gcode += f'G01 Z{verification_z}\n'
+        gcode += 'M0\n'
+
+        gcode += f'G00 Z{travel_z}\n'
+        gcode += f'G00 X{self.click_points[1][0]} Y{self.click_points[1][1]}\n'
+        gcode += f'G01 Z{verification_z}\n'
+        gcode += 'M0\n'
+
+        gcode += f'G00 Z{travel_z}\n'
+        gcode += f'G00 X0 Y0\n'
+        gcode += f'G00 Z{toolchange_z}\n'
+
+        gcode += 'M2'
+
+        self.gcode_editor_tab = TextEditor(app=self.app, plain_text=True)
+
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Gcode Viewer"))
+        self.gcode_editor_tab.setObjectName('gcode_viewer_tab')
+
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
+
+        # first clear previous text in text editor (if any)
+        self.gcode_editor_tab.code_editor.clear()
+        self.gcode_editor_tab.code_editor.setReadOnly(False)
+
+        self.gcode_editor_tab.code_editor.completer_enable = False
+        self.gcode_editor_tab.buttonRun.hide()
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
+
+        self.gcode_editor_tab.t_frame.hide()
+        # then append the text from GCode to the text editor
+        try:
+            self.gcode_editor_tab.code_editor.setPlainText(gcode)
+        except Exception as e:
+            self.app.inform.emit('[ERROR] %s %s' % ('ERROR -->', str(e)))
+            return
+
+        self.gcode_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
+
+        self.gcode_editor_tab.t_frame.show()
+        self.app.proc_container.view.set_idle()
+
+        self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
+
+        _filter_ = "G-Code Files (*.nc);;All Files (*.*)"
+        self.gcode_editor_tab.buttonSave.clicked.disconnect()
+        self.gcode_editor_tab.buttonSave.clicked.connect(
+            lambda: self.gcode_editor_tab.handleSaveGCode(name='fc_ver_gcode', filt=_filter_, callback=self.close_tab))
+
+    def calculate_factors(self):
+        origin_x = self.click_points[0][0]
+        origin_y = self.click_points[0][1]
+
+        top_left_x = float('%.*f' % (self.decimals, self.click_points[2][0]))
+        top_left_y = float('%.*f' % (self.decimals, self.click_points[2][1]))
+
+        try:
+            top_left_dx = float('%.*f' % (self.decimals, self.top_left_coordx_found.get_value()))
+        except TypeError:
+            top_left_dx = top_left_x
+
+        try:
+            top_left_dy = float('%.*f' % (self.decimals, self.top_left_coordy_found.get_value()))
+        except TypeError:
+            top_left_dy = top_left_y
+
+        # top_right_x = float('%.*f' % (self.decimals, self.click_points[3][0]))
+        # top_right_y = float('%.*f' % (self.decimals, self.click_points[3][1]))
+
+        # try:
+        #     top_right_dx = float('%.*f' % (self.decimals, self.top_right_coordx_found.get_value()))
+        # except TypeError:
+        #     top_right_dx = top_right_x
+        #
+        # try:
+        #     top_right_dy = float('%.*f' % (self.decimals, self.top_right_coordy_found.get_value()))
+        # except TypeError:
+        #     top_right_dy = top_right_y
+
+        bot_right_x = float('%.*f' % (self.decimals, self.click_points[1][0]))
+        bot_right_y = float('%.*f' % (self.decimals, self.click_points[1][1]))
+
+        try:
+            bot_right_dx = float('%.*f' % (self.decimals, self.bottom_right_coordx_found.get_value()))
+        except TypeError:
+            bot_right_dx = bot_right_x
+
+        try:
+            bot_right_dy = float('%.*f' % (self.decimals, self.bottom_right_coordy_found.get_value()))
+        except TypeError:
+            bot_right_dy = bot_right_y
+
+        # ------------------------------------------------------------------------------- #
+        # --------------------------- FACTORS CALCULUS ---------------------------------- #
+        # ------------------------------------------------------------------------------- #
+        if top_left_dy != float('%.*f' % (self.decimals, 0.0)):
+            # we have scale on Y
+            scale_y = (top_left_dy + top_left_y - origin_y) / (top_left_y - origin_y)
+            self.scaley_entry.set_value(scale_y)
+
+        if top_left_dx != float('%.*f' % (self.decimals, 0.0)):
+            # we have skew on X
+            dx = top_left_dx
+            dy = top_left_y - origin_y
+            skew_angle_x = math.degrees(math.atan(dx / dy))
+
+            self.skewx_entry.set_value(skew_angle_x)
+
+        if bot_right_dx != float('%.*f' % (self.decimals, 0.0)):
+            # we have scale on X
+            scale_x = (bot_right_dx + bot_right_x - origin_x) / (bot_right_x - origin_x)
+            self.scalex_entry.set_value(scale_x)
+
+        if bot_right_dy != float('%.*f' % (self.decimals, 0.0)):
+            # we have skew on Y
+            dx = bot_right_x - origin_x
+            dy = bot_right_dy + origin_y
+            skew_angle_y = math.degrees(math.atan(dy / dx))
+
+            self.skewy_entry.set_value(skew_angle_y)
+
+    def on_cal_button_click(self):
+        # get the FlatCAM object to calibrate
+        selection_index = self.adj_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.adj_object_combo.rootModelIndex())
+
+        try:
+            self.cal_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCalibration.on_cal_button_click() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
+            return 'fail'
+
+        obj_name = self.cal_object.options["name"] + "_calibrated"
+
+        self.app.worker_task.emit({'fcn': self.new_calibrated_object, 'params': [obj_name]})
+
+    def new_calibrated_object(self, obj_name):
+
+        try:
+            origin_x = self.click_points[0][0]
+            origin_y = self.click_points[0][1]
+        except IndexError as e:
+            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
+            return 'fail'
+
+        scalex = self.scalex_entry.get_value()
+        scaley = self.scaley_entry.get_value()
+
+        skewx = self.skewx_entry.get_value()
+        skewy = self.skewy_entry.get_value()
+
+        # create a new object adjusted (calibrated)
+        def initialize_geometry(obj_init, app):
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
+            try:
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
+            except AttributeError:
+                pass
+
+            try:
+                obj_init.apertures = deepcopy(obj.apertures)
+            except AttributeError:
+                pass
+
+            try:
+                if obj.tools:
+                    obj_init.tools = deepcopy(obj.tools)
+            except Exception as e:
+                log.debug("App.on_copy_object() --> %s" % str(e))
+
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+
+            try:
+                obj_init.source_file = deepcopy(obj.source_file)
+            except (AttributeError, TypeError):
+                pass
+
+        def initialize_gerber(obj_init, app):
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
+            try:
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
+            except AttributeError:
+                pass
+
+            try:
+                obj_init.apertures = deepcopy(obj.apertures)
+            except AttributeError:
+                pass
+
+            try:
+                if obj.tools:
+                    obj_init.tools = deepcopy(obj.tools)
+            except Exception as e:
+                log.debug("App.on_copy_object() --> %s" % str(e))
+
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+
+            try:
+                obj_init.source_file = self.export_gerber(obj_name=obj_name, filename=None, local_use=obj_init,
+                                                          use_thread=False)
+            except (AttributeError, TypeError):
+                pass
+
+        def initialize_excellon(obj_init, app):
+            obj_init.tools = deepcopy(obj.tools)
+
+            # drills are offset, so they need to be deep copied
+            obj_init.drills = deepcopy(obj.drills)
+            # slots are offset, so they need to be deep copied
+            obj_init.slots = deepcopy(obj.slots)
+
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+
+            obj_init.create_geometry()
+
+            obj_init.source_file = self.app.export_excellon(obj_name=obj_name, local_use=obj, filename=None,
+                                                            use_thread=False)
+
+        obj = self.cal_object
+        obj_name = obj_name
+
+        if obj is None:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
+            log.debug("ToolCalibration.new_calibrated_object() --> No object to calibrate")
+            return 'fail'
+
+        try:
+            if obj.kind.lower() == 'excellon':
+                self.app.new_object("excellon", str(obj_name), initialize_excellon)
+            elif obj.kind.lower() == 'gerber':
+                self.app.new_object("gerber", str(obj_name), initialize_gerber)
+            elif obj.kind.lower() == 'geometry':
+                self.app.new_object("geometry", str(obj_name), initialize_geometry)
+        except Exception as e:
+            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
+            return "Operation failed: %s" % str(e)
+
+    def disconnect_cal_events(self):
+        # restore the Grid snapping if it was active before
+        if self.grid_status_memory is True:
+            self.app.ui.grid_snap_btn.trigger()
+
+        self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
+        else:
+            self.canvas.graph_event_disconnect(self.mr)
+
+        self.local_connected = False
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.adj_exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.adj_geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+
+# end of file

+ 1563 - 0
flatcamTools/ToolCopperThieving.py

@@ -0,0 +1,1563 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 10/25/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore
+
+import FlatCAMApp
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet, FCEntry
+from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMExcellon
+
+import shapely.geometry.base as base
+from shapely.ops import cascaded_union, unary_union
+from shapely.geometry import Polygon, MultiPolygon, Point, LineString
+from shapely.geometry import box as box
+import shapely.affinity as affinity
+
+import logging
+from copy import deepcopy
+import numpy as np
+from collections import Iterable
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class ToolCopperThieving(FlatCAMTool):
+    work_finished = QtCore.pyqtSignal()
+
+    toolName = _("Copper Thieving Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+
+        self.decimals = self.app.decimals
+        self.units = self.app.defaults['units']
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        # ## Grid Layout
+        i_grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(i_grid_lay)
+        i_grid_lay.setColumnStretch(0, 0)
+        i_grid_lay.setColumnStretch(1, 1)
+
+        self.grb_object_combo = QtWidgets.QComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.setCurrentIndex(1)
+
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which will be added a copper thieving.")
+        )
+
+        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
+        i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.copper_fill_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
+        self.copper_fill_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
+
+        # CLEARANCE #
+        self.clearance_label = QtWidgets.QLabel('%s:' % _("Clearance"))
+        self.clearance_label.setToolTip(
+            _("This set the distance between the copper thieving components\n"
+              "(the polygon fill may be split in multiple polygons)\n"
+              "and the copper traces in the Gerber file.")
+        )
+        self.clearance_entry = FCDoubleSpinner()
+        self.clearance_entry.set_range(0.00001, 9999.9999)
+        self.clearance_entry.set_precision(self.decimals)
+        self.clearance_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.clearance_label, 1, 0)
+        grid_lay.addWidget(self.clearance_entry, 1, 1)
+
+        # MARGIN #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_range(0.0, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 2, 0)
+        grid_lay.addWidget(self.margin_entry, 2, 1)
+
+        # Reference #
+        self.reference_radio = RadioSet([
+            {'label': _('Itself'), 'value': 'itself'},
+            {"label": _("Area Selection"), "value": "area"},
+            {'label':  _("Reference Object"), 'value': 'box'}
+        ], orientation='vertical', stretch=False)
+        self.reference_label = QtWidgets.QLabel(_("Reference:"))
+        self.reference_label.setToolTip(
+            _("- 'Itself' - the copper thieving extent is based on the object that is copper cleared.\n"
+              "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
+              "- 'Reference Object' - will do copper thieving within the area specified by another object.")
+        )
+        grid_lay.addWidget(self.reference_label, 3, 0)
+        grid_lay.addWidget(self.reference_radio, 3, 1)
+
+        self.box_combo_type_label = QtWidgets.QLabel('%s:' % _("Ref. Type"))
+        self.box_combo_type_label.setToolTip(
+            _("The type of FlatCAM object to be used as copper thieving reference.\n"
+              "It can be Gerber, Excellon or Geometry.")
+        )
+        self.box_combo_type = QtWidgets.QComboBox()
+        self.box_combo_type.addItem(_("Reference Gerber"))
+        self.box_combo_type.addItem(_("Reference Excellon"))
+        self.box_combo_type.addItem(_("Reference Geometry"))
+
+        grid_lay.addWidget(self.box_combo_type_label, 4, 0)
+        grid_lay.addWidget(self.box_combo_type, 4, 1)
+
+        self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
+        self.box_combo_label.setToolTip(
+            _("The FlatCAM object to be used as non copper clearing reference.")
+        )
+        self.box_combo = QtWidgets.QComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(1)
+
+        grid_lay.addWidget(self.box_combo_label, 5, 0)
+        grid_lay.addWidget(self.box_combo, 5, 1)
+
+        self.box_combo.hide()
+        self.box_combo_label.hide()
+        self.box_combo_type.hide()
+        self.box_combo_type_label.hide()
+
+        # Bounding Box Type #
+        self.bbox_type_radio = RadioSet([
+            {'label': _('Rectangular'), 'value': 'rect'},
+            {"label": _("Minimal"), "value": "min"}
+        ], stretch=False)
+        self.bbox_type_label = QtWidgets.QLabel(_("Box Type:"))
+        self.bbox_type_label.setToolTip(
+            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
+              "- 'Minimal' - the bounding box will be the convex hull shape.")
+        )
+        grid_lay.addWidget(self.bbox_type_label, 6, 0)
+        grid_lay.addWidget(self.bbox_type_radio, 6, 1)
+        self.bbox_type_label.hide()
+        self.bbox_type_radio.hide()
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 7, 0, 1, 2)
+
+        # Fill Type
+        self.fill_type_radio = RadioSet([
+            {'label': _('Solid'), 'value': 'solid'},
+            {"label": _("Dots Grid"), "value": "dot"},
+            {"label": _("Squares Grid"), "value": "square"},
+            {"label": _("Lines Grid"), "value": "line"}
+        ], orientation='vertical', stretch=False)
+        self.fill_type_label = QtWidgets.QLabel(_("Fill Type:"))
+        self.fill_type_label.setToolTip(
+            _("- 'Solid' - copper thieving will be a solid polygon.\n"
+              "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
+              "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
+              "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
+        )
+        grid_lay.addWidget(self.fill_type_label, 8, 0)
+        grid_lay.addWidget(self.fill_type_radio, 8, 1)
+
+        # DOTS FRAME
+        self.dots_frame = QtWidgets.QFrame()
+        self.dots_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.dots_frame)
+        dots_grid = QtWidgets.QGridLayout()
+        dots_grid.setColumnStretch(0, 0)
+        dots_grid.setColumnStretch(1, 1)
+        dots_grid.setContentsMargins(0, 0, 0, 0)
+        self.dots_frame.setLayout(dots_grid)
+        self.dots_frame.hide()
+
+        self.dots_label = QtWidgets.QLabel('<b>%s</b>:' % _("Dots Grid Parameters"))
+        dots_grid.addWidget(self.dots_label, 0, 0, 1, 2)
+
+        # Dot diameter #
+        self.dotdia_label = QtWidgets.QLabel('%s:' % _("Dia"))
+        self.dotdia_label.setToolTip(
+            _("Dot diameter in Dots Grid.")
+        )
+        self.dot_dia_entry = FCDoubleSpinner()
+        self.dot_dia_entry.set_range(0.0, 9999.9999)
+        self.dot_dia_entry.set_precision(self.decimals)
+        self.dot_dia_entry.setSingleStep(0.1)
+
+        dots_grid.addWidget(self.dotdia_label, 1, 0)
+        dots_grid.addWidget(self.dot_dia_entry, 1, 1)
+
+        # Dot spacing #
+        self.dotspacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.dotspacing_label.setToolTip(
+            _("Distance between each two dots in Dots Grid.")
+        )
+        self.dot_spacing_entry = FCDoubleSpinner()
+        self.dot_spacing_entry.set_range(0.0, 9999.9999)
+        self.dot_spacing_entry.set_precision(self.decimals)
+        self.dot_spacing_entry.setSingleStep(0.1)
+
+        dots_grid.addWidget(self.dotspacing_label, 2, 0)
+        dots_grid.addWidget(self.dot_spacing_entry, 2, 1)
+
+        # SQUARES FRAME
+        self.squares_frame = QtWidgets.QFrame()
+        self.squares_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.squares_frame)
+        squares_grid = QtWidgets.QGridLayout()
+        squares_grid.setColumnStretch(0, 0)
+        squares_grid.setColumnStretch(1, 1)
+        squares_grid.setContentsMargins(0, 0, 0, 0)
+        self.squares_frame.setLayout(squares_grid)
+        self.squares_frame.hide()
+
+        self.squares_label = QtWidgets.QLabel('<b>%s</b>:' % _("Squares Grid Parameters"))
+        squares_grid.addWidget(self.squares_label, 0, 0, 1, 2)
+
+        # Square Size #
+        self.square_size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.square_size_label.setToolTip(
+            _("Square side size in Squares Grid.")
+        )
+        self.square_size_entry = FCDoubleSpinner()
+        self.square_size_entry.set_range(0.0, 9999.9999)
+        self.square_size_entry.set_precision(self.decimals)
+        self.square_size_entry.setSingleStep(0.1)
+
+        squares_grid.addWidget(self.square_size_label, 1, 0)
+        squares_grid.addWidget(self.square_size_entry, 1, 1)
+
+        # Squares spacing #
+        self.squares_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.squares_spacing_label.setToolTip(
+            _("Distance between each two squares in Squares Grid.")
+        )
+        self.squares_spacing_entry = FCDoubleSpinner()
+        self.squares_spacing_entry.set_range(0.0, 9999.9999)
+        self.squares_spacing_entry.set_precision(self.decimals)
+        self.squares_spacing_entry.setSingleStep(0.1)
+
+        squares_grid.addWidget(self.squares_spacing_label, 2, 0)
+        squares_grid.addWidget(self.squares_spacing_entry, 2, 1)
+
+        # LINES FRAME
+        self.lines_frame = QtWidgets.QFrame()
+        self.lines_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.lines_frame)
+        lines_grid = QtWidgets.QGridLayout()
+        lines_grid.setColumnStretch(0, 0)
+        lines_grid.setColumnStretch(1, 1)
+        lines_grid.setContentsMargins(0, 0, 0, 0)
+        self.lines_frame.setLayout(lines_grid)
+        self.lines_frame.hide()
+
+        self.lines_label = QtWidgets.QLabel('<b>%s</b>:' % _("Lines Grid Parameters"))
+        lines_grid.addWidget(self.lines_label, 0, 0, 1, 2)
+
+        # Square Size #
+        self.line_size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.line_size_label.setToolTip(
+            _("Line thickness size in Lines Grid.")
+        )
+        self.line_size_entry = FCDoubleSpinner()
+        self.line_size_entry.set_range(0.0, 9999.9999)
+        self.line_size_entry.set_precision(self.decimals)
+        self.line_size_entry.setSingleStep(0.1)
+
+        lines_grid.addWidget(self.line_size_label, 1, 0)
+        lines_grid.addWidget(self.line_size_entry, 1, 1)
+
+        # Lines spacing #
+        self.lines_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.lines_spacing_label.setToolTip(
+            _("Distance between each two lines in Lines Grid.")
+        )
+        self.lines_spacing_entry = FCDoubleSpinner()
+        self.lines_spacing_entry.set_range(0.0, 9999.9999)
+        self.lines_spacing_entry.set_precision(self.decimals)
+        self.lines_spacing_entry.setSingleStep(0.1)
+
+        lines_grid.addWidget(self.lines_spacing_label, 2, 0)
+        lines_grid.addWidget(self.lines_spacing_entry, 2, 1)
+
+        # ## Insert Copper Thieving
+        self.fill_button = QtWidgets.QPushButton(_("Insert Copper thieving"))
+        self.fill_button.setToolTip(
+            _("Will add a polygon (may be split in multiple parts)\n"
+              "that will surround the actual Gerber traces at a certain distance.")
+        )
+        self.fill_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.fill_button)
+
+        # ## Grid Layout
+        grid_lay_1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay_1)
+        grid_lay_1.setColumnStretch(0, 0)
+        grid_lay_1.setColumnStretch(1, 1)
+        grid_lay_1.setColumnStretch(2, 0)
+
+        separator_line_1 = QtWidgets.QFrame()
+        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3)
+
+        grid_lay_1.addWidget(QtWidgets.QLabel(''))
+
+        self.robber_bar_label = QtWidgets.QLabel('<b>%s</b>' % _('Robber Bar Parameters'))
+        self.robber_bar_label.setToolTip(
+            _("Parameters used for the robber bar.\n"
+              "Robber bar = copper border to help in pattern hole plating.")
+        )
+        grid_lay_1.addWidget(self.robber_bar_label, 1, 0, 1, 3)
+
+        # ROBBER BAR MARGIN #
+        self.rb_margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.rb_margin_label.setToolTip(
+            _("Bounding box margin for robber bar.")
+        )
+        self.rb_margin_entry = FCDoubleSpinner()
+        self.rb_margin_entry.set_range(-9999.9999, 9999.9999)
+        self.rb_margin_entry.set_precision(self.decimals)
+        self.rb_margin_entry.setSingleStep(0.1)
+
+        grid_lay_1.addWidget(self.rb_margin_label, 2, 0)
+        grid_lay_1.addWidget(self.rb_margin_entry, 2, 1, 1, 2)
+
+        # THICKNESS #
+        self.rb_thickness_label = QtWidgets.QLabel('%s:' % _("Thickness"))
+        self.rb_thickness_label.setToolTip(
+            _("The robber bar thickness.")
+        )
+        self.rb_thickness_entry = FCDoubleSpinner()
+        self.rb_thickness_entry.set_range(0.0000, 9999.9999)
+        self.rb_thickness_entry.set_precision(self.decimals)
+        self.rb_thickness_entry.setSingleStep(0.1)
+
+        grid_lay_1.addWidget(self.rb_thickness_label, 3, 0)
+        grid_lay_1.addWidget(self.rb_thickness_entry, 3, 1, 1, 2)
+
+        # ## Insert Robber Bar
+        self.rb_button = QtWidgets.QPushButton(_("Insert Robber Bar"))
+        self.rb_button.setToolTip(
+            _("Will add a polygon with a defined thickness\n"
+              "that will surround the actual Gerber object\n"
+              "at a certain distance.\n"
+              "Required when doing holes pattern plating.")
+        )
+        self.rb_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay_1.addWidget(self.rb_button, 4, 0, 1, 3)
+
+        separator_line_2 = QtWidgets.QFrame()
+        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay_1.addWidget(separator_line_2, 5, 0, 1, 3)
+
+        self.patern_mask_label = QtWidgets.QLabel('<b>%s</b>' % _('Pattern Plating Mask'))
+        self.patern_mask_label.setToolTip(
+            _("Generate a mask for pattern plating.")
+        )
+        grid_lay_1.addWidget(self.patern_mask_label, 6, 0, 1, 3)
+
+        self.sm_obj_label = QtWidgets.QLabel("%s:" % _("Select Soldermask object"))
+        self.sm_obj_label.setToolTip(
+            _("Gerber Object with the soldermask.\n"
+              "It will be used as a base for\n"
+              "the pattern plating mask.")
+        )
+
+        self.sm_object_combo = QtWidgets.QComboBox()
+        self.sm_object_combo.setModel(self.app.collection)
+        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_object_combo.setCurrentIndex(1)
+
+        grid_lay_1.addWidget(self.sm_obj_label, 7, 0, 1, 3)
+        grid_lay_1.addWidget(self.sm_object_combo, 8, 0, 1, 3)
+
+        # Openings CLEARANCE #
+        self.clearance_ppm_label = QtWidgets.QLabel('%s:' % _("Clearance"))
+        self.clearance_ppm_label.setToolTip(
+            _("The distance between the possible copper thieving elements\n"
+              "and/or robber bar and the actual openings in the mask.")
+        )
+        self.clearance_ppm_entry = FCDoubleSpinner()
+        self.clearance_ppm_entry.set_range(-9999.9999, 9999.9999)
+        self.clearance_ppm_entry.set_precision(self.decimals)
+        self.clearance_ppm_entry.setSingleStep(0.1)
+
+        grid_lay_1.addWidget(self.clearance_ppm_label, 9, 0)
+        grid_lay_1.addWidget(self.clearance_ppm_entry, 9, 1, 1, 2)
+
+        # Plated area
+        self.plated_area_label = QtWidgets.QLabel('%s:' % _("Plated area"))
+        self.plated_area_label.setToolTip(
+            _("The area to be plated by pattern plating.\n"
+              "Basically is made from the openings in the plating mask.\n\n"
+              "<<WARNING>> - the calculated area is actually a bit larger\n"
+              "due of the fact that the soldermask openings are by design\n"
+              "a bit larger than the copper pads, and this area is\n"
+              "calculated from the soldermask openings.")
+        )
+        self.plated_area_entry = FCEntry()
+        self.plated_area_entry.setDisabled(True)
+
+        if self.units.upper() == 'MM':
+            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("mm"))
+        else:
+            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("in"))
+
+        grid_lay_1.addWidget(self.plated_area_label, 10, 0)
+        grid_lay_1.addWidget(self.plated_area_entry, 10, 1)
+        grid_lay_1.addWidget(self.units_area_label, 10, 2)
+
+        # ## Pattern Plating Mask
+        self.ppm_button = QtWidgets.QPushButton(_("Generate pattern plating mask"))
+        self.ppm_button.setToolTip(
+            _("Will add to the soldermask gerber geometry\n"
+              "the geometries of the copper thieving and/or\n"
+              "the robber bar if those were generated.")
+        )
+        self.ppm_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay_1.addWidget(self.ppm_button, 11, 0, 1, 3)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        # Objects involved in Copper thieving
+        self.grb_object = None
+        self.ref_obj = None
+        self.sel_rect = list()
+        self.sm_object = None
+
+        # store the flattened geometry here:
+        self.flat_geometry = list()
+
+        # Events ID
+        self.mr = None
+        self.mm = None
+
+        # Mouse cursor positions
+        self.mouse_is_dragging = False
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+
+        self.area_method = False
+
+        # Tool properties
+        self.clearance_val = None
+        self.margin_val = None
+        self.geo_steps_per_circle = 128
+
+        # Thieving geometry storage
+        self.new_solid_geometry = list()
+
+        # Robber bar geometry storage
+        self.robber_geo = None
+        self.robber_line = None
+
+        self.rb_thickness = None
+
+        # SIGNALS
+        self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
+        self.reference_radio.group_toggle_fn = self.on_toggle_reference
+        self.fill_type_radio.activated_custom.connect(self.on_thieving_type)
+
+        self.fill_button.clicked.connect(self.execute)
+        self.rb_button.clicked.connect(self.add_robber_bar)
+        self.ppm_button.clicked.connect(self.on_add_ppm)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+        self.work_finished.connect(self.on_new_pattern_plating_object)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolCopperThieving()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            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, _("Copper Thieving Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+F', **kwargs)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units']
+        self.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"]))
+        self.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
+        self.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
+        self.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"])
+        self.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"])
+        self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"])
+
+        self.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"])
+        self.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"])
+        self.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"])
+        self.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"])
+        self.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"])
+        self.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"])
+
+        self.rb_margin_entry.set_value(self.app.defaults["tools_copper_thieving_rb_margin"])
+        self.rb_thickness_entry.set_value(self.app.defaults["tools_copper_thieving_rb_thickness"])
+        self.clearance_ppm_entry.set_value(self.app.defaults["tools_copper_thieving_mask_clearance"])
+
+        # INIT SECTION
+        self.area_method = False
+        self.robber_geo = None
+        self.robber_line = None
+        self.new_solid_geometry = None
+
+    def on_combo_box_type(self):
+        obj_type = self.box_combo_type.currentIndex()
+        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(0)
+
+    def on_toggle_reference(self):
+        if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area":
+            self.box_combo.hide()
+            self.box_combo_label.hide()
+            self.box_combo_type.hide()
+            self.box_combo_type_label.hide()
+        else:
+            self.box_combo.show()
+            self.box_combo_label.show()
+            self.box_combo_type.show()
+            self.box_combo_type_label.show()
+
+        if self.reference_radio.get_value() == "itself":
+            self.bbox_type_label.show()
+            self.bbox_type_radio.show()
+        else:
+            if self.fill_type_radio.get_value() == 'line':
+                self.reference_radio.set_value('itself')
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
+                return
+
+            self.bbox_type_label.hide()
+            self.bbox_type_radio.hide()
+
+    def on_thieving_type(self, choice):
+        if choice == 'solid':
+            self.dots_frame.hide()
+            self.squares_frame.hide()
+            self.lines_frame.hide()
+            self.app.inform.emit(_("Solid fill selected."))
+        elif choice == 'dot':
+            self.dots_frame.show()
+            self.squares_frame.hide()
+            self.lines_frame.hide()
+            self.app.inform.emit(_("Dots grid fill selected."))
+        elif choice == 'square':
+            self.dots_frame.hide()
+            self.squares_frame.show()
+            self.lines_frame.hide()
+            self.app.inform.emit(_("Squares grid fill selected."))
+        else:
+            if self.reference_radio.get_value() != 'itself':
+                self.reference_radio.set_value('itself')
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
+            self.dots_frame.hide()
+            self.squares_frame.hide()
+            self.lines_frame.show()
+
+    def add_robber_bar(self):
+        rb_margin = self.rb_margin_entry.get_value()
+        self.rb_thickness = self.rb_thickness_entry.get_value()
+
+        # get the Gerber object on which the Robber bar will be inserted
+        selection_index = self.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.add_robber_bar() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        try:
+            outline_pol = self.grb_object.solid_geometry.envelope
+        except TypeError:
+            outline_pol = MultiPolygon(self.grb_object.solid_geometry).envelope
+
+        rb_distance = rb_margin + (self.rb_thickness / 2.0)
+        self.robber_line = outline_pol.buffer(rb_distance).exterior
+
+        self.robber_geo = self.robber_line.buffer(self.rb_thickness / 2.0)
+
+        self.app.proc_container.update_view_text(' %s' % _("Append geometry"))
+
+        aperture_found = None
+        for ap_id, ap_val in self.grb_object.apertures.items():
+            if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness:
+                aperture_found = ap_id
+                break
+
+        if aperture_found:
+            geo_elem = dict()
+            geo_elem['solid'] = self.robber_geo
+            geo_elem['follow'] = self.robber_line
+            self.grb_object.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
+        else:
+            ap_keys = list(self.grb_object.apertures.keys())
+            if ap_keys:
+                new_apid = str(int(max(ap_keys)) + 1)
+            else:
+                new_apid = '10'
+
+            self.grb_object.apertures[new_apid] = dict()
+            self.grb_object.apertures[new_apid]['type'] = 'C'
+            self.grb_object.apertures[new_apid]['size'] = self.rb_thickness
+            self.grb_object.apertures[new_apid]['geometry'] = list()
+
+            geo_elem = dict()
+            geo_elem['solid'] = self.robber_geo
+            geo_elem['follow'] = self.robber_line
+            self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
+
+        geo_obj = self.grb_object.solid_geometry
+        if isinstance(geo_obj, MultiPolygon):
+            s_list = list()
+            for pol in geo_obj.geoms:
+                s_list.append(pol)
+            s_list.append(self.robber_geo)
+            geo_obj = MultiPolygon(s_list)
+        elif isinstance(geo_obj, list):
+            geo_obj.append(self.robber_geo)
+        elif isinstance(geo_obj, Polygon):
+            geo_obj = MultiPolygon([geo_obj, self.robber_geo])
+
+        self.grb_object.solid_geometry = geo_obj
+
+        self.app.proc_container.update_view_text(' %s' % _("Append source file"))
+        # update the source file with the new geometry:
+        self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
+                                                             filename=None,
+                                                             local_use=self.grb_object,
+                                                             use_thread=False)
+        self.app.proc_container.update_view_text(' %s' % '')
+        self.on_exit()
+        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
+
+    def execute(self):
+        self.app.call_source = "copper_thieving_tool"
+
+        self.clearance_val = self.clearance_entry.get_value()
+        self.margin_val = self.margin_entry.get_value()
+        reference_method = self.reference_radio.get_value()
+
+        # get the Gerber object on which the Copper thieving will be inserted
+        selection_index = self.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.execute() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        if reference_method == 'itself':
+            bound_obj_name = self.grb_object_combo.currentText()
+
+            # Get reference object.
+            try:
+                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
+                return "Could not retrieve object: %s" % self.obj_name
+
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
+
+        elif reference_method == 'area':
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
+
+            self.area_method = True
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+                self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+                self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+
+            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+            self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+
+        elif reference_method == 'box':
+            bound_obj_name = self.box_combo.currentText()
+
+            # Get reference object.
+            try:
+                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
+                return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e))
+
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                ref_obj=self.ref_obj,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
+
+        # To be called after clicking on the plot.
+
+    def on_mouse_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        event_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        # do clear area only for left mouse clicks
+        if event.button == 1:
+            if self.first_click is False:
+                self.first_click = True
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
+
+                self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                if self.app.grid_status() is True:
+                    self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+            else:
+                self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                self.app.delete_selection_shape()
+
+                if self.app.grid_status() is True:
+                    curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+                else:
+                    curr_pos = (event_pos[0], event_pos[1])
+
+                x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+                x1, y1 = curr_pos[0], curr_pos[1]
+                pt1 = (x0, y0)
+                pt2 = (x1, y0)
+                pt3 = (x1, y1)
+                pt4 = (x0, y1)
+
+                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                self.sel_rect.append(new_rectangle)
+
+                # add a temporary shape on canvas
+                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+                self.first_click = False
+                return
+
+        elif event.button == right_button and self.mouse_is_dragging is False:
+            self.area_method = False
+            self.first_click = False
+
+            self.delete_tool_selection_shape()
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            if len(self.sel_rect) == 0:
+                return
+
+            self.sel_rect = cascaded_union(self.sel_rect)
+
+            if not isinstance(self.sel_rect, Iterable):
+                self.sel_rect = [self.sel_rect]
+
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                ref_obj=self.sel_rect,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
+
+    # called on mouse move
+    def on_mouse_move(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
+
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        # detect mouse dragging motion
+        if event_is_dragging is True:
+            self.mouse_is_dragging = True
+        else:
+            self.mouse_is_dragging = False
+
+        # update the cursor position
+        if self.app.grid_status() is True:
+            # Update cursor
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+
+            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         size=self.app.defaults["global_cursor_size"])
+
+        # update the positions on status bar
+        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
+        if self.cursor_pos is None:
+            self.cursor_pos = (0, 0)
+
+        dx = curr_pos[0] - float(self.cursor_pos[0])
+        dy = curr_pos[1] - float(self.cursor_pos[1])
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+
+        # draw the utility geometry
+        if self.first_click:
+            self.app.delete_selection_shape()
+            self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                 coords=(curr_pos[0], curr_pos[1]))
+
+    def on_copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
+        """
+
+        :param thieving_obj:
+        :param ref_obj:
+        :param c_val:
+        :param margin:
+        :param run_threaded:
+        :return:
+        """
+
+        if run_threaded:
+            proc = self.app.proc_container.new('%s ...' % _("Thieving"))
+        else:
+            QtWidgets.QApplication.processEvents()
+
+        self.app.proc_container.view.set_busy('%s ...' % _("Thieving"))
+
+        # #####################################################################
+        # ####### Read the parameters #########################################
+        # #####################################################################
+
+        log.debug("Copper Thieving Tool started. Reading parameters.")
+        self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
+
+        ref_selected = self.reference_radio.get_value()
+        if c_val is None:
+            c_val = float(self.app.defaults["tools_copperfill_clearance"])
+        if margin is None:
+            margin = float(self.app.defaults["tools_copperfill_margin"])
+
+        fill_type = self.fill_type_radio.get_value()
+        dot_dia = self.dot_dia_entry.get_value()
+        dot_spacing = self.dot_spacing_entry.get_value()
+        square_size = self.square_size_entry.get_value()
+        square_spacing = self.squares_spacing_entry.get_value()
+        line_size = self.line_size_entry.get_value()
+        line_spacing = self.lines_spacing_entry.get_value()
+
+        # make sure that the source object solid geometry is an Iterable
+        if not isinstance(self.grb_object.solid_geometry, Iterable):
+            self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
+
+        def job_thread_thieving(app_obj):
+            # #########################################################################################
+            # Prepare isolation polygon. This will create the clearance over the Gerber features ######
+            # #########################################################################################
+            log.debug("Copper Thieving Tool. Preparing isolation polygons.")
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
+
+            # variables to display the percentage of work done
+            geo_len = 0
+            try:
+                for pol in app_obj.grb_object.solid_geometry:
+                    geo_len += 1
+            except TypeError:
+                geo_len = 1
+
+            old_disp_number = 0
+            pol_nr = 0
+
+            clearance_geometry = []
+            try:
+                for pol in app_obj.grb_object.solid_geometry:
+                    if app_obj.app.abort_flag:
+                        # graceful abort requested by the user
+                        raise FlatCAMApp.GracefulException
+
+                    clearance_geometry.append(
+                        pol.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
+                    )
+
+                    pol_nr += 1
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                    if old_disp_number < disp_number <= 100:
+                        app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
+                                                                    (_("Thieving"), int(disp_number)))
+                        old_disp_number = disp_number
+            except TypeError:
+                # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+                # MultiPolygon (not an iterable)
+                clearance_geometry.append(
+                    app_obj.grb_object.solid_geometry.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
+                )
+
+            app_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering"))
+            clearance_geometry = unary_union(clearance_geometry)
+
+            # #########################################################################################
+            # Prepare the area to fill with copper. ###################################################
+            # #########################################################################################
+            log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
+
+            try:
+                if ref_obj is None or ref_obj == 'itself':
+                    working_obj = thieving_obj
+                else:
+                    working_obj = ref_obj
+            except Exception as e:
+                log.debug("ToolCopperThieving.on_copper_thieving() --> %s" % str(e))
+                return 'fail'
+
+            app_obj.app.proc_container.update_view_text(' %s' % _("Working..."))
+            if ref_selected == 'itself':
+                geo_n = working_obj.solid_geometry
+
+                try:
+                    if app_obj.bbox_type_radio.get_value() == 'min':
+                        if isinstance(geo_n, MultiPolygon):
+                            env_obj = geo_n.convex_hull
+                        elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
+                                (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
+                            env_obj = cascaded_union(geo_n)
+                        else:
+                            env_obj = cascaded_union(geo_n)
+                            env_obj = env_obj.convex_hull
+                        bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                    else:
+                        if isinstance(geo_n, Polygon):
+                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
+                        elif isinstance(geo_n, list):
+                            geo_n = unary_union(geo_n)
+                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
+                        elif isinstance(geo_n, MultiPolygon):
+                            x0, y0, x1, y1 = geo_n.bounds
+                            geo = box(x0, y0, x1, y1)
+                            bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                        else:
+                            app_obj.app.inform.emit(
+                                '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
+                            )
+                            return 'fail'
+
+                except Exception as e:
+                    log.debug("ToolCopperFIll.on_copper_thieving()  'itself'  --> %s" % str(e))
+                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
+                    return 'fail'
+            elif ref_selected == 'area':
+                geo_buff_list = []
+                try:
+                    for poly in working_obj:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise FlatCAMApp.GracefulException
+                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+                except TypeError:
+                    geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+
+                bounding_box = MultiPolygon(geo_buff_list)
+            else:   # ref_selected == 'box'
+                geo_n = working_obj.solid_geometry
+
+                if isinstance(working_obj, FlatCAMGeometry):
+                    try:
+                        __ = iter(geo_n)
+                    except Exception as e:
+                        log.debug("ToolCopperFIll.on_copper_thieving() 'box' --> %s" % str(e))
+                        geo_n = [geo_n]
+
+                    geo_buff_list = []
+                    for poly in geo_n:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise FlatCAMApp.GracefulException
+                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+
+                    bounding_box = cascaded_union(geo_buff_list)
+                elif isinstance(working_obj, FlatCAMGerber):
+                    geo_n = cascaded_union(geo_n).convex_hull
+                    bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
+                    bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                else:
+                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
+                    return 'fail'
+
+            log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
+
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
+
+            # #########################################################################################
+            # ########## Generate filling geometry. ###################################################
+            # #########################################################################################
+
+            app_obj.new_solid_geometry = bounding_box.difference(clearance_geometry)
+
+            # determine the bounding box polygon for the entire Gerber object to which we add copper thieving
+            # if isinstance(geo_n, list):
+            #     env_obj = unary_union(geo_n).buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+            # else:
+            #     env_obj = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+            #
+            # x0, y0, x1, y1 = env_obj.bounds
+            # bounding_box = box(x0, y0, x1, y1)
+            app_obj.app.proc_container.update_view_text(' %s' % _("Create geometry"))
+
+            bounding_box = thieving_obj.solid_geometry.envelope.buffer(
+                distance=margin,
+                join_style=base.JOIN_STYLE.mitre
+            )
+            x0, y0, x1, y1 = bounding_box.bounds
+
+            if fill_type == 'dot' or fill_type == 'square':
+                # build the MultiPolygon of dots/squares that will fill the entire bounding box
+                thieving_list = list()
+
+                if fill_type == 'dot':
+                    radius = dot_dia / 2.0
+                    new_x = x0 + radius
+                    new_y = y0 + radius
+                    while new_x <= x1 - radius:
+                        while new_y <= y1 - radius:
+                            dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
+                            thieving_list.append(dot_geo)
+                            new_y += dot_dia + dot_spacing
+                        new_x += dot_dia + dot_spacing
+                        new_y = y0 + radius
+                else:
+                    h_size = square_size / 2.0
+                    new_x = x0 + h_size
+                    new_y = y0 + h_size
+                    while new_x <= x1 - h_size:
+                        while new_y <= y1 - h_size:
+                            a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
+                            square_geo = box(a, b, c, d)
+                            thieving_list.append(square_geo)
+                            new_y += square_size + square_spacing
+                        new_x += square_size + square_spacing
+                        new_y = y0 + h_size
+
+                thieving_box_geo = MultiPolygon(thieving_list)
+                dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
+                dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
+
+                thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
+
+                try:
+                    _it = iter(app_obj.new_solid_geometry)
+                except TypeError:
+                    app_obj.new_solid_geometry = [app_obj.new_solid_geometry]
+
+                try:
+                    _it = iter(thieving_box_geo)
+                except TypeError:
+                    thieving_box_geo = [thieving_box_geo]
+
+                thieving_geo = list()
+                for dot_geo in thieving_box_geo:
+                    for geo_t in app_obj.new_solid_geometry:
+                        if dot_geo.within(geo_t):
+                            thieving_geo.append(dot_geo)
+
+                app_obj.new_solid_geometry = thieving_geo
+
+            if fill_type == 'line':
+                half_thick_line = line_size / 2.0
+
+                # create a thick polygon-line that surrounds the copper features
+                outline_geometry = []
+                try:
+                    for pol in app_obj.grb_object.solid_geometry:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise FlatCAMApp.GracefulException
+
+                        outline_geometry.append(
+                            pol.buffer(c_val+half_thick_line, int(int(app_obj.geo_steps_per_circle) / 4))
+                        )
+
+                        pol_nr += 1
+                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                        if old_disp_number < disp_number <= 100:
+                            app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
+                                                                        (_("Buffering"), int(disp_number)))
+                            old_disp_number = disp_number
+                except TypeError:
+                    # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+                    # MultiPolygon (not an iterable)
+                    outline_geometry.append(
+                        app_obj.grb_object.solid_geometry.buffer(
+                            c_val+half_thick_line,
+                            int(int(app_obj.geo_steps_per_circle) / 4)
+                        )
+                    )
+
+                app_obj.app.proc_container.update_view_text(' %s' % _("Buffering"))
+                outline_geometry = unary_union(outline_geometry)
+
+                outline_line = list()
+                try:
+                    for geo_o in outline_geometry:
+                        outline_line.append(
+                            geo_o.exterior.buffer(
+                                half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                            )
+                        )
+                except TypeError:
+                    outline_line.append(
+                        outline_geometry.exterior.buffer(
+                            half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                        )
+                    )
+
+                outline_geometry = unary_union(outline_line)
+
+                # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
+                box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
+                box_outline_geo_exterior = box_outline_geo.exterior
+                box_outline_geometry = box_outline_geo_exterior.buffer(
+                    half_thick_line,
+                    resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                )
+
+                bx0, by0, bx1, by1 = box_outline_geo.bounds
+                thieving_lines_geo = list()
+                new_x = bx0
+                new_y = by0
+                while new_x <= x1 - half_thick_line:
+                    line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
+                        half_thick_line,
+                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                    )
+                    thieving_lines_geo.append(line_geo)
+                    new_x += line_size + line_spacing
+
+                while new_y <= y1 - half_thick_line:
+                    line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
+                        half_thick_line,
+                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                    )
+                    thieving_lines_geo.append(line_geo)
+                    new_y += line_size + line_spacing
+
+                # merge everything together
+                diff_lines_geo = list()
+                for line_poly in thieving_lines_geo:
+                    rest_line = line_poly.difference(clearance_geometry)
+                    diff_lines_geo.append(rest_line)
+                app_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo])
+                app_obj.new_solid_geometry = app_obj.flat_geometry
+
+            app_obj.app.proc_container.update_view_text(' %s' % _("Append geometry"))
+            geo_list = app_obj.grb_object.solid_geometry
+            if isinstance(app_obj.grb_object.solid_geometry, MultiPolygon):
+                geo_list = list(app_obj.grb_object.solid_geometry.geoms)
+
+            if '0' not in app_obj.grb_object.apertures:
+                app_obj.grb_object.apertures['0'] = dict()
+                app_obj.grb_object.apertures['0']['geometry'] = list()
+                app_obj.grb_object.apertures['0']['type'] = 'REG'
+                app_obj.grb_object.apertures['0']['size'] = 0.0
+
+            try:
+                for poly in app_obj.new_solid_geometry:
+                    # append to the new solid geometry
+                    geo_list.append(poly)
+
+                    # append into the '0' aperture
+                    geo_elem = dict()
+                    geo_elem['solid'] = poly
+                    geo_elem['follow'] = poly.exterior
+                    app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
+            except TypeError:
+                # append to the new solid geometry
+                geo_list.append(app_obj.new_solid_geometry)
+
+                # append into the '0' aperture
+                geo_elem = dict()
+                geo_elem['solid'] = app_obj.new_solid_geometry
+                geo_elem['follow'] = app_obj.new_solid_geometry.exterior
+                app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
+
+            app_obj.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
+
+            app_obj.app.proc_container.update_view_text(' %s' % _("Append source file"))
+            # update the source file with the new geometry:
+            app_obj.grb_object.source_file = app_obj.app.export_gerber(obj_name=app_obj.grb_object.options['name'],
+                                                                       filename=None,
+                                                                       local_use=app_obj.grb_object,
+                                                                       use_thread=False)
+            app_obj.app.proc_container.update_view_text(' %s' % '')
+            app_obj.on_exit()
+            app_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
+
+        if run_threaded:
+            self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]})
+        else:
+            job_thread_thieving(self)
+
+    def on_add_ppm(self):
+        run_threaded = True
+
+        if run_threaded:
+            proc = self.app.proc_container.new('%s ...' % _("P-Plating Mask"))
+        else:
+            QtWidgets.QApplication.processEvents()
+
+        self.app.proc_container.view.set_busy('%s ...' % _("P-Plating Mask"))
+
+        if run_threaded:
+            self.app.worker_task.emit({'fcn': self.on_new_pattern_plating_object, 'params': []})
+        else:
+            self.on_new_pattern_plating_object()
+
+    def on_new_pattern_plating_object(self):
+        # get the Gerber object on which the Copper thieving will be inserted
+        selection_index = self.sm_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.sm_object_combo.rootModelIndex())
+
+        try:
+            self.sm_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_add_ppm() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        ppm_clearance = self.clearance_ppm_entry.get_value()
+        rb_thickness = self.rb_thickness
+
+        self.app.proc_container.update_view_text(' %s' % _("Append PP-M geometry"))
+        geo_list = self.sm_object.solid_geometry
+        if isinstance(self.sm_object.solid_geometry, MultiPolygon):
+            geo_list = list(self.sm_object.solid_geometry.geoms)
+
+        # if the clearance is negative apply it to the original soldermask too
+        if ppm_clearance < 0:
+            temp_geo_list = list()
+            for geo in geo_list:
+                temp_geo_list.append(geo.buffer(ppm_clearance))
+            geo_list = temp_geo_list
+
+        plated_area = 0.0
+        for geo in geo_list:
+            plated_area += geo.area
+
+        if self.new_solid_geometry:
+            for geo in self.new_solid_geometry:
+                plated_area += geo.area
+        if self.robber_geo:
+            plated_area += self.robber_geo.area
+        self.plated_area_entry.set_value(plated_area)
+
+        thieving_solid_geo = self.new_solid_geometry
+        robber_solid_geo = self.robber_geo
+        robber_line = self.robber_line
+
+        def obj_init(grb_obj, app_obj):
+            grb_obj.multitool = False
+            grb_obj.source_file = list()
+            grb_obj.multigeo = False
+            grb_obj.follow = False
+            grb_obj.apertures = dict()
+            grb_obj.solid_geometry = list()
+
+            # try:
+            #     grb_obj.options['xmin'] = 0
+            #     grb_obj.options['ymin'] = 0
+            #     grb_obj.options['xmax'] = 0
+            #     grb_obj.options['ymax'] = 0
+            # except KeyError:
+            #     pass
+
+            # if we have copper thieving geometry, add it
+            if thieving_solid_geo:
+                if '0' not in grb_obj.apertures:
+                    grb_obj.apertures['0'] = dict()
+                    grb_obj.apertures['0']['geometry'] = list()
+                    grb_obj.apertures['0']['type'] = 'REG'
+                    grb_obj.apertures['0']['size'] = 0.0
+
+                try:
+                    for poly in thieving_solid_geo:
+                        poly_b = poly.buffer(ppm_clearance)
+
+                        # append to the new solid geometry
+                        geo_list.append(poly_b)
+
+                        # append into the '0' aperture
+                        geo_elem = dict()
+                        geo_elem['solid'] = poly_b
+                        geo_elem['follow'] = poly_b.exterior
+                        grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
+                except TypeError:
+                    # append to the new solid geometry
+                    geo_list.append(thieving_solid_geo.buffer(ppm_clearance))
+
+                    # append into the '0' aperture
+                    geo_elem = dict()
+                    geo_elem['solid'] = thieving_solid_geo.buffer(ppm_clearance)
+                    geo_elem['follow'] = thieving_solid_geo.buffer(ppm_clearance).exterior
+                    grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
+
+            # if we have robber bar geometry, add it
+            if robber_solid_geo:
+                aperture_found = None
+                for ap_id, ap_val in grb_obj.apertures.items():
+                    if ap_val['type'] == 'C' and ap_val['size'] == app_obj.rb_thickness + ppm_clearance:
+                        aperture_found = ap_id
+                        break
+
+                if aperture_found:
+                    geo_elem = dict()
+                    geo_elem['solid'] = robber_solid_geo
+                    geo_elem['follow'] = robber_line
+                    grb_obj.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
+                else:
+                    ap_keys = list(grb_obj.apertures.keys())
+                    max_apid = int(max(ap_keys))
+                    if ap_keys and max_apid != 0:
+                        new_apid = str(max_apid + 1)
+                    else:
+                        new_apid = '10'
+
+                    grb_obj.apertures[new_apid] = dict()
+                    grb_obj.apertures[new_apid]['type'] = 'C'
+                    grb_obj.apertures[new_apid]['size'] = rb_thickness + ppm_clearance
+                    grb_obj.apertures[new_apid]['geometry'] = list()
+
+                    geo_elem = dict()
+                    geo_elem['solid'] = robber_solid_geo.buffer(ppm_clearance)
+                    geo_elem['follow'] = Polygon(robber_line).buffer(ppm_clearance / 2.0).exterior
+                    grb_obj.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
+
+                geo_list.append(robber_solid_geo.buffer(ppm_clearance))
+
+            grb_obj.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
+
+            app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
+            # update the source file with the new geometry:
+            grb_obj.source_file = app_obj.export_gerber(obj_name=name,
+                                                        filename=None,
+                                                        local_use=grb_obj,
+                                                        use_thread=False)
+            app_obj.proc_container.update_view_text(' %s' % '')
+
+        # Object name
+        obj_name, separatpr, obj_extension = self.sm_object.options['name'].rpartition('.')
+        name = '%s_%s.%s' % (obj_name, 'plating_mask', obj_extension)
+
+        self.app.new_object('gerber', name, obj_init, autoselected=False)
+
+        # Register recent file
+        self.app.file_opened.emit("gerber", name)
+
+        self.on_exit()
+        self.app.inform.emit('[success] %s' % _("Generating Pattern Plating Mask done."))
+
+    def replot(self, obj):
+        def worker_task():
+            with self.app.proc_container.new('%s...' % _("Plotting")):
+                obj.plot()
+
+        self.app.worker_task.emit({'fcn': worker_task, 'params': []})
+
+    def on_exit(self):
+        # plot the objects
+        if self.grb_object:
+            self.replot(obj=self.grb_object)
+
+        if self.sm_object:
+            self.replot(obj=self.sm_object)
+
+        # update the bounding box values
+        try:
+            a, b, c, d = self.grb_object.bounds()
+            self.grb_object.options['xmin'] = a
+            self.grb_object.options['ymin'] = b
+            self.grb_object.options['xmax'] = c
+            self.grb_object.options['ymax'] = d
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_exit() bounds -> copper thieving Gerber error --> %s" % str(e))
+
+        # update the bounding box values
+        try:
+            a, b, c, d = self.sm_object.bounds()
+            self.sm_object.options['xmin'] = a
+            self.sm_object.options['ymin'] = b
+            self.sm_object.options['xmax'] = c
+            self.sm_object.options['ymax'] = d
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_exit() bounds -> pattern plating mask error --> %s" % str(e))
+
+        # reset the variables
+        self.grb_object = None
+        self.sm_object = None
+        self.ref_obj = None
+        self.sel_rect = list()
+
+        # Events ID
+        self.mr = None
+        self.mm = None
+
+        # Mouse cursor positions
+        self.mouse_is_dragging = False
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+
+        # if True it means we exited from tool in the middle of area adding therefore disconnect the events
+        if self.area_method is True:
+            self.app.delete_selection_shape()
+            self.area_method = False
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+        self.app.call_source = "app"
+        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))
+
+    def flatten(self, geometry):
+        """
+        Creates a list of non-iterable linear geometry objects.
+        :param geometry: Shapely type or list or list of list of such.
+
+        Results are placed in self.flat_geometry
+        """
+
+        # ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                if geo is not None:
+                    self.flatten(geometry=geo)
+
+        # ## Not iterable, do the actual indexing and add.
+        except TypeError:
+            self.flat_geometry.append(geometry)
+
+        return self.flat_geometry

+ 158 - 84
flatcamTools/ToolCutOut.py

@@ -7,7 +7,7 @@
 
 
 from PyQt5 import QtWidgets, QtGui, QtCore
 from PyQt5 import QtWidgets, QtGui, QtCore
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
+from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox, OptionalInputSection
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMObj import FlatCAMGerber
 
 
 from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing
 from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing
@@ -30,6 +30,12 @@ if '_' not in builtins.__dict__:
 
 
 log = logging.getLogger('base')
 log = logging.getLogger('base')
 
 
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
 
 
 class CutOut(FlatCAMTool):
 class CutOut(FlatCAMTool):
 
 
@@ -40,7 +46,7 @@ class CutOut(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -54,8 +60,10 @@ class CutOut(FlatCAMTool):
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
 
 
         # Form Layout
         # Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
 
 
         # Type of object to be cutout
         # Type of object to be cutout
         self.type_obj_combo = QtWidgets.QComboBox()
         self.type_obj_combo = QtWidgets.QComboBox()
@@ -69,7 +77,7 @@ class CutOut(FlatCAMTool):
         # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
         # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
 
 
-        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Obj Type"))
+        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
         self.type_obj_combo_label.setToolTip(
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be cutout.\n"
             _("Specify the type of object to be cutout.\n"
               "It can be of type: Gerber or Geometry.\n"
               "It can be of type: Gerber or Geometry.\n"
@@ -77,7 +85,11 @@ class CutOut(FlatCAMTool):
               "of objects that will populate the 'Object' combobox.")
               "of objects that will populate the 'Object' combobox.")
         )
         )
         self.type_obj_combo_label.setMinimumWidth(60)
         self.type_obj_combo_label.setMinimumWidth(60)
-        form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
+        grid0.addWidget(self.type_obj_combo_label, 0, 0)
+        grid0.addWidget(self.type_obj_combo, 0, 1)
+
+        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Object to be cutout"))
+        self.object_label.setToolTip('%s.' % _("Object to be cutout"))
 
 
         # Object to be cutout
         # Object to be cutout
         self.obj_combo = QtWidgets.QComboBox()
         self.obj_combo = QtWidgets.QComboBox()
@@ -85,14 +97,11 @@ class CutOut(FlatCAMTool):
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setCurrentIndex(1)
         self.obj_combo.setCurrentIndex(1)
 
 
-        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
-        self.object_label.setToolTip(
-            _("Object to be cutout.                        ")
-        )
-        form_layout.addRow(self.object_label, self.obj_combo)
+        grid0.addWidget(self.object_label, 1, 0, 1, 2)
+        grid0.addWidget(self.obj_combo, 2, 0, 1, 2)
 
 
         # Object kind
         # Object kind
-        self.kindlabel = QtWidgets.QLabel('%s:' % _('Obj kind'))
+        self.kindlabel = QtWidgets.QLabel('%s:' % _('Object kind'))
         self.kindlabel.setToolTip(
         self.kindlabel.setToolTip(
             _("Choice of what kind the object we want to cutout is.<BR>"
             _("Choice of what kind the object we want to cutout is.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
@@ -103,43 +112,95 @@ class CutOut(FlatCAMTool):
             {"label": _("Single"), "value": "single"},
             {"label": _("Single"), "value": "single"},
             {"label": _("Panel"), "value": "panel"},
             {"label": _("Panel"), "value": "panel"},
         ])
         ])
-        form_layout.addRow(self.kindlabel, self.obj_kind_combo)
+        grid0.addWidget(self.kindlabel, 3, 0)
+        grid0.addWidget(self.obj_kind_combo, 3, 1)
 
 
         # Tool Diameter
         # Tool Diameter
         self.dia = FCDoubleSpinner()
         self.dia = FCDoubleSpinner()
         self.dia.set_precision(self.decimals)
         self.dia.set_precision(self.decimals)
+        self.dia.set_range(0.0000, 9999.9999)
 
 
-        self.dia_label = QtWidgets.QLabel('%s:' % _("Tool dia"))
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
         self.dia_label.setToolTip(
         self.dia_label.setToolTip(
            _("Diameter of the tool used to cutout\n"
            _("Diameter of the tool used to cutout\n"
              "the PCB shape out of the surrounding material.")
              "the PCB shape out of the surrounding material.")
         )
         )
-        form_layout.addRow(self.dia_label, self.dia)
+        grid0.addWidget(self.dia_label, 4, 0)
+        grid0.addWidget(self.dia, 4, 1)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _(
+                "Cutting depth (negative)\n"
+                "below the copper surface."
+            )
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-9999.9999, -0.00001)
+        else:
+            self.cutz_entry.setRange(-9999.9999, 9999.9999)
+
+        self.cutz_entry.setSingleStep(0.1)
+
+        grid0.addWidget(cutzlabel, 5, 0)
+        grid0.addWidget(self.cutz_entry, 5, 1)
+
+        # Multi-pass
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+
+        self.maxdepth_entry = FCDoubleSpinner()
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.setRange(0, 9999.9999)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(
+            _(
+                "Depth of each pass (positive)."
+            )
+        )
+        self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
+
+        grid0.addWidget(self.mpass_cb, 6, 0)
+        grid0.addWidget(self.maxdepth_entry, 6, 1)
 
 
         # Margin
         # Margin
         self.margin = FCDoubleSpinner()
         self.margin = FCDoubleSpinner()
         self.margin.set_precision(self.decimals)
         self.margin.set_precision(self.decimals)
 
 
-        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
         self.margin_label.setToolTip(
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
            _("Margin over bounds. A positive value here\n"
              "will make the cutout of the PCB further from\n"
              "will make the cutout of the PCB further from\n"
              "the actual PCB border")
              "the actual PCB border")
         )
         )
-        form_layout.addRow(self.margin_label, self.margin)
+        grid0.addWidget(self.margin_label, 7, 0)
+        grid0.addWidget(self.margin, 7, 1)
 
 
         # Gapsize
         # Gapsize
         self.gapsize = FCDoubleSpinner()
         self.gapsize = FCDoubleSpinner()
         self.gapsize.set_precision(self.decimals)
         self.gapsize.set_precision(self.decimals)
 
 
-        self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
+        self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size"))
         self.gapsize_label.setToolTip(
         self.gapsize_label.setToolTip(
            _("The size of the bridge gaps in the cutout\n"
            _("The size of the bridge gaps in the cutout\n"
              "used to keep the board connected to\n"
              "used to keep the board connected to\n"
              "the surrounding material (the one \n"
              "the surrounding material (the one \n"
              "from which the PCB is cutout).")
              "from which the PCB is cutout).")
         )
         )
-        form_layout.addRow(self.gapsize_label, self.gapsize)
+        grid0.addWidget(self.gapsize_label, 8, 0)
+        grid0.addWidget(self.gapsize, 8, 1)
 
 
         # How gaps wil be rendered:
         # How gaps wil be rendered:
         # lr    - left + right
         # lr    - left + right
@@ -150,13 +211,18 @@ class CutOut(FlatCAMTool):
         # 8     - 2*left + 2*right +2*top + 2*bottom
         # 8     - 2*left + 2*right +2*top + 2*bottom
 
 
         # Surrounding convex box shape
         # Surrounding convex box shape
-        self.convex_box = FCCheckBox()
-        self.convex_box_label = QtWidgets.QLabel('%s:' % _("Convex Sh."))
-        self.convex_box_label.setToolTip(
+        self.convex_box = FCCheckBox('%s' % _("Convex Shape"))
+        # self.convex_box_label = QtWidgets.QLabel('%s' % _("Convex Sh."))
+        self.convex_box.setToolTip(
             _("Create a convex shape surrounding the entire PCB.\n"
             _("Create a convex shape surrounding the entire PCB.\n"
               "Used only if the source object type is Gerber.")
               "Used only if the source object type is Gerber.")
         )
         )
-        form_layout.addRow(self.convex_box_label, self.convex_box)
+        grid0.addWidget(self.convex_box, 9, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 10, 0, 1, 2)
 
 
         # Title2
         # Title2
         title_param_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('A. Automatic Bridge Gaps'))
         title_param_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('A. Automatic Bridge Gaps'))
@@ -193,46 +259,39 @@ class CutOut(FlatCAMTool):
         form_layout_2.addRow(gaps_label, self.gaps)
         form_layout_2.addRow(gaps_label, self.gaps)
 
 
         # Buttons
         # Buttons
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
-
-        title_ff_label = QtWidgets.QLabel("<b>%s:</b>" % _('FreeForm'))
-        title_ff_label.setToolTip(
-            _("The cutout shape can be of ny shape.\n"
-              "Useful when the PCB has a non-rectangular shape.")
-        )
-        hlay.addWidget(title_ff_label)
-
-        hlay.addStretch()
-
-        self.ff_cutout_object_btn = QtWidgets.QPushButton(_("Generate Geo"))
+        self.ff_cutout_object_btn = QtWidgets.QPushButton(_("Generate Freeform Geometry"))
         self.ff_cutout_object_btn.setToolTip(
         self.ff_cutout_object_btn.setToolTip(
             _("Cutout the selected object.\n"
             _("Cutout the selected object.\n"
               "The cutout shape can be of any shape.\n"
               "The cutout shape can be of any shape.\n"
               "Useful when the PCB has a non-rectangular shape.")
               "Useful when the PCB has a non-rectangular shape.")
         )
         )
-        hlay.addWidget(self.ff_cutout_object_btn)
-
-        hlay2 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay2)
-
-        title_rct_label = QtWidgets.QLabel("<b>%s:</b>" % _('Rectangular'))
-        title_rct_label.setToolTip(
-            _("The resulting cutout shape is\n"
-              "always a rectangle shape and it will be\n"
-              "the bounding box of the Object.")
-        )
-        hlay2.addWidget(title_rct_label)
+        self.ff_cutout_object_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.ff_cutout_object_btn)
 
 
-        hlay2.addStretch()
-        self.rect_cutout_object_btn = QtWidgets.QPushButton(_("Generate Geo"))
+        self.rect_cutout_object_btn = QtWidgets.QPushButton(_("Generate Rectangular Geometry"))
         self.rect_cutout_object_btn.setToolTip(
         self.rect_cutout_object_btn.setToolTip(
             _("Cutout the selected object.\n"
             _("Cutout the selected object.\n"
               "The resulting cutout shape is\n"
               "The resulting cutout shape is\n"
               "always a rectangle shape and it will be\n"
               "always a rectangle shape and it will be\n"
               "the bounding box of the Object.")
               "the bounding box of the Object.")
         )
         )
-        hlay2.addWidget(self.rect_cutout_object_btn)
+        self.rect_cutout_object_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.rect_cutout_object_btn)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
 
 
         # Title5
         # Title5
         title_manual_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('B. Manual Bridge Gaps'))
         title_manual_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('B. Manual Bridge Gaps'))
@@ -253,51 +312,33 @@ class CutOut(FlatCAMTool):
         self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.man_object_combo.setCurrentIndex(1)
         self.man_object_combo.setCurrentIndex(1)
 
 
-        self.man_object_label = QtWidgets.QLabel('%s:' % _("Geo Obj"))
+        self.man_object_label = QtWidgets.QLabel('%s:' % _("Geometry Object"))
         self.man_object_label.setToolTip(
         self.man_object_label.setToolTip(
             _("Geometry object used to create the manual cutout.")
             _("Geometry object used to create the manual cutout.")
         )
         )
         self.man_object_label.setMinimumWidth(60)
         self.man_object_label.setMinimumWidth(60)
-        # e_lab_0 = QtWidgets.QLabel('')
-
-        form_layout_3.addRow(self.man_object_label, self.man_object_combo)
-        # form_layout_3.addRow(e_lab_0)
 
 
-        hlay3 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay3)
+        form_layout_3.addRow(self.man_object_label)
+        form_layout_3.addRow(self.man_object_combo)
 
 
-        self.man_geo_label = QtWidgets.QLabel('%s:' % _("Manual Geo"))
-        self.man_geo_label.setToolTip(
-            _("If the object to be cutout is a Gerber\n"
-              "first create a Geometry that surrounds it,\n"
-              "to be used as the cutout, if one doesn't exist yet.\n"
-              "Select the source Gerber file in the top object combobox.")
-        )
-        hlay3.addWidget(self.man_geo_label)
+        # form_layout_3.addRow(e_lab_0)
 
 
-        hlay3.addStretch()
-        self.man_geo_creation_btn = QtWidgets.QPushButton(_("Generate Geo"))
+        self.man_geo_creation_btn = QtWidgets.QPushButton(_("Generate Manual Geometry"))
         self.man_geo_creation_btn.setToolTip(
         self.man_geo_creation_btn.setToolTip(
             _("If the object to be cutout is a Gerber\n"
             _("If the object to be cutout is a Gerber\n"
               "first create a Geometry that surrounds it,\n"
               "first create a Geometry that surrounds it,\n"
               "to be used as the cutout, if one doesn't exist yet.\n"
               "to be used as the cutout, if one doesn't exist yet.\n"
               "Select the source Gerber file in the top object combobox.")
               "Select the source Gerber file in the top object combobox.")
         )
         )
-        hlay3.addWidget(self.man_geo_creation_btn)
-
-        hlay4 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay4)
-
-        self.man_bridge_gaps_label = QtWidgets.QLabel('%s:' % _("Manual Add Bridge Gaps"))
-        self.man_bridge_gaps_label.setToolTip(
-            _("Use the left mouse button (LMB) click\n"
-              "to create a bridge gap to separate the PCB from\n"
-              "the surrounding material.")
-        )
-        hlay4.addWidget(self.man_bridge_gaps_label)
+        self.man_geo_creation_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.man_geo_creation_btn)
 
 
-        hlay4.addStretch()
-        self.man_gaps_creation_btn = QtWidgets.QPushButton(_("Generate Gap"))
+        self.man_gaps_creation_btn = QtWidgets.QPushButton(_("Manual Add Bridge Gaps"))
         self.man_gaps_creation_btn.setToolTip(
         self.man_gaps_creation_btn.setToolTip(
             _("Use the left mouse button (LMB) click\n"
             _("Use the left mouse button (LMB) click\n"
               "to create a bridge gap to separate the PCB from\n"
               "to create a bridge gap to separate the PCB from\n"
@@ -305,10 +346,29 @@ class CutOut(FlatCAMTool):
               "The LMB click has to be done on the perimeter of\n"
               "The LMB click has to be done on the perimeter of\n"
               "the Geometry object used as a cutout geometry.")
               "the Geometry object used as a cutout geometry.")
         )
         )
-        hlay4.addWidget(self.man_gaps_creation_btn)
+        self.man_gaps_creation_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.man_gaps_creation_btn)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
         self.cutting_gapsize = 0.0
         self.cutting_gapsize = 0.0
         self.cutting_dia = 0.0
         self.cutting_dia = 0.0
 
 
@@ -339,6 +399,7 @@ class CutOut(FlatCAMTool):
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.man_geo_creation_btn.clicked.connect(self.on_manual_geo)
         self.man_geo_creation_btn.clicked.connect(self.on_manual_geo)
         self.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click)
         self.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def on_type_obj_index_changed(self, index):
     def on_type_obj_index_changed(self, index):
         obj_type = self.type_obj_combo.currentIndex()
         obj_type = self.type_obj_combo.currentIndex()
@@ -373,7 +434,7 @@ class CutOut(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Cutout Tool"))
         self.app.ui.notebook.setTabText(2, _("Cutout Tool"))
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+X', **kwargs)
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
         self.reset_fields()
         self.reset_fields()
@@ -381,6 +442,10 @@ class CutOut(FlatCAMTool):
         self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
         self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
         self.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"])
         self.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"])
         self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
         self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
+        self.cutz_entry.set_value(float(self.app.defaults["tools_cutout_z"]))
+        self.mpass_cb.set_value(float(self.app.defaults["tools_cutout_mdepth"]))
+        self.maxdepth_entry.set_value(float(self.app.defaults["tools_cutout_depthperpass"]))
+
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
         self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
         self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
@@ -547,6 +612,9 @@ class CutOut(FlatCAMTool):
             geo_obj.options['xmax'] = xmax
             geo_obj.options['xmax'] = xmax
             geo_obj.options['ymax'] = ymax
             geo_obj.options['ymax'] = ymax
             geo_obj.options['cnctooldia'] = str(dia)
             geo_obj.options['cnctooldia'] = str(dia)
+            geo_obj.options['cutz'] = self.cutz_entry.get_value()
+            geo_obj.options['multidepth'] = self.mpass_cb.get_value()
+            geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value()
 
 
         outname = cutout_obj.options["name"] + "_cutout"
         outname = cutout_obj.options["name"] + "_cutout"
         self.app.new_object('geometry', outname, geo_init)
         self.app.new_object('geometry', outname, geo_init)
@@ -702,6 +770,9 @@ class CutOut(FlatCAMTool):
 
 
             geo_obj.solid_geometry = deepcopy(solid_geo)
             geo_obj.solid_geometry = deepcopy(solid_geo)
             geo_obj.options['cnctooldia'] = str(dia)
             geo_obj.options['cnctooldia'] = str(dia)
+            geo_obj.options['cutz'] = self.cutz_entry.get_value()
+            geo_obj.options['multidepth'] = self.mpass_cb.get_value()
+            geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value()
 
 
         outname = cutout_obj.options["name"] + "_cutout"
         outname = cutout_obj.options["name"] + "_cutout"
         self.app.new_object('geometry', outname, geo_init)
         self.app.new_object('geometry', outname, geo_init)
@@ -842,6 +913,9 @@ class CutOut(FlatCAMTool):
                         solid_geo.append(poly.exterior)
                         solid_geo.append(poly.exterior)
                     geo_obj.solid_geometry = deepcopy(solid_geo)
                     geo_obj.solid_geometry = deepcopy(solid_geo)
             geo_obj.options['cnctooldia'] = str(dia)
             geo_obj.options['cnctooldia'] = str(dia)
+            geo_obj.options['cutz'] = self.cutz_entry.get_value()
+            geo_obj.options['multidepth'] = self.mpass_cb.get_value()
+            geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value()
 
 
         outname = cutout_obj.options["name"] + "_cutout"
         outname = cutout_obj.options["name"] + "_cutout"
         self.app.new_object('geometry', outname, geo_init)
         self.app.new_object('geometry', outname, geo_init)

+ 63 - 35
flatcamTools/ToolDblSided.py

@@ -26,7 +26,7 @@ class DblSidedTool(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -45,6 +45,8 @@ class DblSidedTool(FlatCAMTool):
         # ## Grid Layout
         # ## Grid Layout
         grid_lay = QtWidgets.QGridLayout()
         grid_lay = QtWidgets.QGridLayout()
         self.layout.addLayout(grid_lay)
         self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
 
 
         # ## Gerber Object to mirror
         # ## Gerber Object to mirror
         self.gerber_object_combo = QtWidgets.QComboBox()
         self.gerber_object_combo = QtWidgets.QComboBox()
@@ -53,9 +55,7 @@ class DblSidedTool(FlatCAMTool):
         self.gerber_object_combo.setCurrentIndex(1)
         self.gerber_object_combo.setCurrentIndex(1)
 
 
         self.botlay_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
         self.botlay_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.botlay_label.setToolTip(
-            "Gerber  to be mirrored."
-        )
+        self.botlay_label.setToolTip('%s.' % _("Gerber to be mirrored"))
 
 
         self.mirror_gerber_button = QtWidgets.QPushButton(_("Mirror"))
         self.mirror_gerber_button = QtWidgets.QPushButton(_("Mirror"))
         self.mirror_gerber_button.setToolTip(
         self.mirror_gerber_button.setToolTip(
@@ -63,6 +63,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
               "object, but modifies it.")
         )
         )
+        self.mirror_gerber_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_gerber_button.setMinimumWidth(60)
         self.mirror_gerber_button.setMinimumWidth(60)
 
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -77,9 +83,7 @@ class DblSidedTool(FlatCAMTool):
         self.exc_object_combo.setCurrentIndex(1)
         self.exc_object_combo.setCurrentIndex(1)
 
 
         self.excobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("EXCELLON"))
         self.excobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("EXCELLON"))
-        self.excobj_label.setToolTip(
-            _("Excellon Object to be mirrored.")
-        )
+        self.excobj_label.setToolTip(_("Excellon Object to be mirrored."))
 
 
         self.mirror_exc_button = QtWidgets.QPushButton(_("Mirror"))
         self.mirror_exc_button = QtWidgets.QPushButton(_("Mirror"))
         self.mirror_exc_button.setToolTip(
         self.mirror_exc_button.setToolTip(
@@ -87,6 +91,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
               "object, but modifies it.")
         )
         )
+        self.mirror_exc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_exc_button.setMinimumWidth(60)
         self.mirror_exc_button.setMinimumWidth(60)
 
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -111,6 +121,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
               "object, but modifies it.")
         )
         )
+        self.mirror_geo_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_geo_button.setMinimumWidth(60)
         self.mirror_geo_button.setMinimumWidth(60)
 
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -126,9 +142,8 @@ class DblSidedTool(FlatCAMTool):
         self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
         self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
                                      {'label': 'Y', 'value': 'Y'}])
                                      {'label': 'Y', 'value': 'Y'}])
         self.mirax_label = QtWidgets.QLabel(_("Mirror Axis:"))
         self.mirax_label = QtWidgets.QLabel(_("Mirror Axis:"))
-        self.mirax_label.setToolTip(
-            _("Mirror vertically (X) or horizontally (Y).")
-        )
+        self.mirax_label.setToolTip(_("Mirror vertically (X) or horizontally (Y)."))
+
         # grid_lay.addRow("Mirror Axis:", self.mirror_axis)
         # grid_lay.addRow("Mirror Axis:", self.mirror_axis)
         self.empty_lb1 = QtWidgets.QLabel("")
         self.empty_lb1 = QtWidgets.QLabel("")
         grid_lay1.addWidget(self.empty_lb1, 6, 0)
         grid_lay1.addWidget(self.empty_lb1, 6, 0)
@@ -154,6 +169,8 @@ class DblSidedTool(FlatCAMTool):
         # ## Grid Layout
         # ## Grid Layout
         grid_lay2 = QtWidgets.QGridLayout()
         grid_lay2 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid_lay2)
         self.layout.addLayout(grid_lay2)
+        grid_lay2.setColumnStretch(0, 1)
+        grid_lay2.setColumnStretch(1, 0)
 
 
         # ## Point/Box
         # ## Point/Box
         self.point_box_container = QtWidgets.QVBoxLayout()
         self.point_box_container = QtWidgets.QVBoxLayout()
@@ -172,6 +189,12 @@ class DblSidedTool(FlatCAMTool):
               "The (x, y) coordinates are captured by pressing SHIFT key\n"
               "The (x, y) coordinates are captured by pressing SHIFT key\n"
               "and left mouse button click on canvas or you can enter the coords manually.")
               "and left mouse button click on canvas or you can enter the coords manually.")
         )
         )
+        self.add_point_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.add_point_button.setMinimumWidth(60)
         self.add_point_button.setMinimumWidth(60)
 
 
         grid_lay2.addWidget(self.pb_label, 10, 0)
         grid_lay2.addWidget(self.pb_label, 10, 0)
@@ -187,9 +210,9 @@ class DblSidedTool(FlatCAMTool):
         self.box_combo.setCurrentIndex(1)
         self.box_combo.setCurrentIndex(1)
 
 
         self.box_combo_type = QtWidgets.QComboBox()
         self.box_combo_type = QtWidgets.QComboBox()
-        self.box_combo_type.addItem(_("Gerber   Reference Box Object"))
-        self.box_combo_type.addItem(_("Excellon Reference Box Object"))
-        self.box_combo_type.addItem(_("Geometry Reference Box Object"))
+        self.box_combo_type.addItem(_("Reference Gerber"))
+        self.box_combo_type.addItem(_("Reference Excellon"))
+        self.box_combo_type.addItem(_("Reference Geometry"))
 
 
         self.point_box_container.addWidget(self.box_combo_type)
         self.point_box_container.addWidget(self.box_combo_type)
         self.point_box_container.addWidget(self.box_combo)
         self.point_box_container.addWidget(self.box_combo)
@@ -222,6 +245,12 @@ class DblSidedTool(FlatCAMTool):
               "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
               "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
               "- by entering the coords manually in the format: (x1, y1), (x2, y2), ...")
               "- by entering the coords manually in the format: (x1, y1), (x2, y2), ...")
         )
         )
+        self.add_drill_point_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.add_drill_point_button.setMinimumWidth(60)
         self.add_drill_point_button.setMinimumWidth(60)
 
 
         grid_lay3.addWidget(self.alignment_holes, 0, 0)
         grid_lay3.addWidget(self.alignment_holes, 0, 0)
@@ -252,9 +281,6 @@ class DblSidedTool(FlatCAMTool):
         grid0.addWidget(self.dd_label, 1, 0)
         grid0.addWidget(self.dd_label, 1, 0)
         grid0.addWidget(self.drill_dia, 1, 1)
         grid0.addWidget(self.drill_dia, 1, 1)
 
 
-        hlay2 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay2)
-
         # ## Buttons
         # ## Buttons
         self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
         self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
         self.create_alignment_hole_button.setToolTip(
         self.create_alignment_hole_button.setToolTip(
@@ -262,16 +288,28 @@ class DblSidedTool(FlatCAMTool):
               "specified alignment holes and their mirror\n"
               "specified alignment holes and their mirror\n"
               "images.")
               "images.")
         )
         )
-        hlay2.addWidget(self.create_alignment_hole_button)
+        self.create_alignment_hole_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.create_alignment_hole_button)
 
 
-        self.reset_button = QtWidgets.QPushButton(_("Reset"))
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
         self.reset_button.setToolTip(
         self.reset_button.setToolTip(
-            _("Resets all the fields.")
+            _("Will reset the tool parameters.")
         )
         )
-        self.reset_button.setMinimumWidth(60)
-        hlay2.addWidget(self.reset_button)
-
-        self.layout.addStretch()
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
 
 
         # ## Signals
         # ## Signals
         self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
         self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
@@ -369,22 +407,12 @@ class DblSidedTool(FlatCAMTool):
 
 
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
 
-        try:
-            dia = float(self.drill_dia.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                dia = float(self.drill_dia.get_value().replace(',', '.'))
-                self.drill_dia.set_value(dia)
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool diameter value is missing or wrong format. "
-                                                              "Add it and retry."))
-                return
-
+        dia = float(self.drill_dia.get_value())
         if dia is '':
         if dia is '':
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("No value or wrong format in Drill Dia entry. Add it and retry."))
                                  _("No value or wrong format in Drill Dia entry. Add it and retry."))
             return
             return
+
         tools = {"1": {"C": dia}}
         tools = {"1": {"C": dia}}
 
 
         # holes = self.alignment_holes.get_value()
         # holes = self.alignment_holes.get_value()

+ 12 - 6
flatcamTools/ToolDistance.py

@@ -11,7 +11,7 @@ from FlatCAMTool import FlatCAMTool
 from flatcamGUI.VisPyVisuals import *
 from flatcamGUI.VisPyVisuals import *
 from flatcamGUI.GUIElements import FCEntry
 from flatcamGUI.GUIElements import FCEntry
 
 
-import copy
+from copy import copy
 import math
 import math
 import logging
 import logging
 import gettext
 import gettext
@@ -33,8 +33,10 @@ class Distance(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.app = app
         self.app = app
+        self.decimals = self.app.decimals
+
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
@@ -135,8 +137,6 @@ class Distance(FlatCAMTool):
         self.mm = None
         self.mm = None
         self.mr = None
         self.mr = None
 
 
-        self.decimals = 4
-
         # VisPy visuals
         # VisPy visuals
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
             self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
@@ -182,7 +182,7 @@ class Distance(FlatCAMTool):
 
 
         # Switch notebook to tool page
         # Switch notebook to tool page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         self.app.command_active = "Distance"
         self.app.command_active = "Distance"
 
 
@@ -194,6 +194,12 @@ class Distance(FlatCAMTool):
         self.distance_y_entry.set_value('0.0')
         self.distance_y_entry.set_value('0.0')
         self.angle_entry.set_value('0.0')
         self.angle_entry.set_value('0.0')
         self.total_distance_entry.set_value('0.0')
         self.total_distance_entry.set_value('0.0')
+
+        # this is a hack; seems that triggering the grid will make the visuals better
+        # trigger it twice to return to the original state
+        self.app.ui.grid_snap_btn.trigger()
+        self.app.ui.grid_snap_btn.trigger()
+
         log.debug("Distance Tool --> tool initialized")
         log.debug("Distance Tool --> tool initialized")
 
 
     def activate_measure_tool(self):
     def activate_measure_tool(self):
@@ -204,7 +210,7 @@ class Distance(FlatCAMTool):
         self.original_call_source = copy(self.app.call_source)
         self.original_call_source = copy(self.app.call_source)
 
 
         self.app.inform.emit(_("MEASURING: Click on the Start point ..."))
         self.app.inform.emit(_("MEASURING: Click on the Start point ..."))
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         # we can connect the app mouse events to the measurement tool
         # we can connect the app mouse events to the measurement tool
         # NEVER DISCONNECT THOSE before connecting some other handlers; it breaks something in VisPy
         # NEVER DISCONNECT THOSE before connecting some other handlers; it breaks something in VisPy

+ 4 - 4
flatcamTools/ToolDistanceMin.py

@@ -35,7 +35,8 @@ class DistanceMin(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
+        self.decimals = self.app.decimals
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
@@ -137,7 +138,6 @@ class DistanceMin(FlatCAMTool):
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
-        self.decimals = 4
         self.h_point = (0, 0)
         self.h_point = (0, 0)
 
 
         self.measure_btn.clicked.connect(self.activate_measure_tool)
         self.measure_btn.clicked.connect(self.activate_measure_tool)
@@ -175,7 +175,7 @@ class DistanceMin(FlatCAMTool):
         # Switch notebook to tool page
         # Switch notebook to tool page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         # initial view of the layout
         # initial view of the layout
         self.start_entry.set_value('(0, 0)')
         self.start_entry.set_value('(0, 0)')
@@ -195,7 +195,7 @@ class DistanceMin(FlatCAMTool):
         # ENABLE the Measuring TOOL
         # ENABLE the Measuring TOOL
         self.jump_hp_btn.setDisabled(False)
         self.jump_hp_btn.setDisabled(False)
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         if self.app.call_source == 'app':
         if self.app.call_source == 'app':
             selected_objs = self.app.collection.get_selected()
             selected_objs = self.app.collection.get_selected()

+ 920 - 0
flatcamTools/ToolFiducials.py

@@ -0,0 +1,920 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 11/21/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore
+
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet, EvalEntry, FCTable
+
+from shapely.geometry import Point, Polygon, MultiPolygon, LineString
+from shapely.geometry import box as box
+
+import math
+import logging
+from copy import deepcopy
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class ToolFiducials(FlatCAMTool):
+
+    toolName = _("Fiducials Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+
+        self.decimals = self.app.decimals
+        self.units = ''
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Fiducials Coordinates'))
+        self.points_label.setToolTip(
+            _("A table with the fiducial points coordinates,\n"
+              "in the format (x, y).")
+        )
+        self.layout.addWidget(self.points_label)
+
+        self.points_table = FCTable()
+        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.layout.addWidget(self.points_table)
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        self.points_table.setColumnCount(3)
+        self.points_table.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Name"),
+                _("Coordinates"),
+            ]
+        )
+        self.points_table.setRowCount(3)
+        row = 0
+
+        flags = QtCore.Qt.ItemIsEnabled
+
+        # BOTTOM LEFT
+        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
+        id_item_1.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
+
+        self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left'))
+        self.bottom_left_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.bottom_left_coords_lbl)
+        self.bottom_left_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry)
+        row += 1
+
+        # TOP RIGHT
+        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
+        id_item_2.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+
+        self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right'))
+        self.top_right_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.top_right_coords_lbl)
+        self.top_right_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coords_entry)
+        row += 1
+
+        # Second Point
+        self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
+        self.id_item_3.setFlags(flags)
+        self.points_table.setItem(row, 0, self.id_item_3)  # Tool name/id
+
+        self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point'))
+        self.sec_point_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.sec_point_coords_lbl)
+        self.sec_points_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry)
+
+        vertical_header = self.points_table.verticalHeader()
+        vertical_header.hide()
+        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.points_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+
+        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        # for x in range(4):
+        #     self.points_table.resizeColumnToContents(x)
+        self.points_table.resizeColumnsToContents()
+        self.points_table.resizeRowsToContents()
+
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
+        self.points_table.setMaximumHeight(self.points_table.getHeight() + 2)
+
+        # remove the frame on the QLineEdit childrens of the table
+        for row in range(self.points_table.rowCount()):
+            self.points_table.cellWidget(row, 2).setFrame(False)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # DIAMETER #
+        self.size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.size_label.setToolTip(
+            _("This set the fiducial diameter if fiducial type is circular,\n"
+              "otherwise is the size of the fiducial.\n"
+              "The soldermask opening is double than that.")
+        )
+        self.fid_size_entry = FCDoubleSpinner()
+        self.fid_size_entry.set_range(1.0000, 3.0000)
+        self.fid_size_entry.set_precision(self.decimals)
+        self.fid_size_entry.setWrapping(True)
+        self.fid_size_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.size_label, 1, 0)
+        grid_lay.addWidget(self.fid_size_entry, 1, 1)
+
+        # MARGIN #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_range(-9999.9999, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 2, 0)
+        grid_lay.addWidget(self.margin_entry, 2, 1)
+
+        # Mode #
+        self.mode_radio = RadioSet([
+            {'label': _('Auto'), 'value': 'auto'},
+            {"label": _("Manual"), "value": "manual"}
+        ], stretch=False)
+        self.mode_label = QtWidgets.QLabel(_("Mode:"))
+        self.mode_label.setToolTip(
+            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
+              "- 'Manual' - manual placement of fiducials.")
+        )
+        grid_lay.addWidget(self.mode_label, 3, 0)
+        grid_lay.addWidget(self.mode_radio, 3, 1)
+
+        # Position for second fiducial #
+        self.pos_radio = RadioSet([
+            {'label': _('Up'), 'value': 'up'},
+            {"label": _("Down"), "value": "down"},
+            {"label": _("None"), "value": "no"}
+        ], stretch=False)
+        self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
+        self.pos_label.setToolTip(
+            _("The position for the second fiducial.\n"
+              "- 'Up' - the order is: bottom-left, top-left, top-right.\n "
+              "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
+              "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
+        )
+        grid_lay.addWidget(self.pos_label, 4, 0)
+        grid_lay.addWidget(self.pos_radio, 4, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Fiducial type #
+        self.fid_type_radio = RadioSet([
+            {'label': _('Circular'), 'value': 'circular'},
+            {"label": _("Cross"), "value": "cross"},
+            {"label": _("Chess"), "value": "chess"}
+        ], stretch=False)
+        self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
+        self.fid_type_label.setToolTip(
+            _("The type of fiducial.\n"
+              "- 'Circular' - this is the regular fiducial.\n"
+              "- 'Cross' - cross lines fiducial.\n"
+              "- 'Chess' - chess pattern fiducial.")
+        )
+        grid_lay.addWidget(self.fid_type_label, 6, 0)
+        grid_lay.addWidget(self.fid_type_radio, 6, 1)
+
+        # Line Thickness #
+        self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
+        self.line_thickness_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.line_thickness_entry = FCDoubleSpinner()
+        self.line_thickness_entry.set_range(0.00001, 9999.9999)
+        self.line_thickness_entry.set_precision(self.decimals)
+        self.line_thickness_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.line_thickness_label, 7, 0)
+        grid_lay.addWidget(self.line_thickness_entry, 7, 1)
+
+        separator_line_1 = QtWidgets.QFrame()
+        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line_1, 8, 0, 1, 2)
+
+        # Copper Gerber object
+        self.grb_object_combo = QtWidgets.QComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.setCurrentIndex(1)
+
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("Copper Gerber"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which will be added a copper thieving.")
+        )
+
+        grid_lay.addWidget(self.grbobj_label, 9, 0, 1, 2)
+        grid_lay.addWidget(self.grb_object_combo, 10, 0, 1, 2)
+
+        # ## Insert Copper Fiducial
+        self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
+        self.add_cfid_button.setToolTip(
+            _("Will add a polygon on the copper layer to serve as fiducial.")
+        )
+        self.add_cfid_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.add_cfid_button, 11, 0, 1, 2)
+
+        separator_line_2 = QtWidgets.QFrame()
+        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line_2, 12, 0, 1, 2)
+
+        # Soldermask Gerber object #
+        self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
+        self.sm_object_label.setToolTip(
+            _("The Soldermask Gerber object.")
+        )
+        self.sm_object_combo = QtWidgets.QComboBox()
+        self.sm_object_combo.setModel(self.app.collection)
+        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_object_combo.setCurrentIndex(1)
+
+        grid_lay.addWidget(self.sm_object_label, 13, 0, 1, 2)
+        grid_lay.addWidget(self.sm_object_combo, 14, 0, 1, 2)
+
+        # ## Insert Soldermask opening for Fiducial
+        self.add_sm_opening_button = QtWidgets.QPushButton(_("Add Soldermask Opening"))
+        self.add_sm_opening_button.setToolTip(
+            _("Will add a polygon on the soldermask layer\n"
+              "to serve as fiducial opening.\n"
+              "The diameter is always double of the diameter\n"
+              "for the copper fiducial.")
+        )
+        self.add_sm_opening_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid_lay.addWidget(self.add_sm_opening_button, 15, 0, 1, 2)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        # Objects involved in Copper thieving
+        self.grb_object = None
+        self.sm_object = None
+
+        self.copper_obj_set = set()
+        self.sm_obj_set = set()
+
+        # store the flattened geometry here:
+        self.flat_geometry = list()
+
+        # Events ID
+        self.mr = None
+        self.mm = None
+
+        # Mouse cursor positions
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+
+        self.mode_method = False
+
+        # Tool properties
+        self.fid_dia = None
+        self.sm_opening_dia = None
+
+        self.margin_val = None
+        self.sec_position = None
+        self.geo_steps_per_circle = 128
+
+        self.click_points = list()
+
+        # SIGNALS
+        self.add_cfid_button.clicked.connect(self.add_fiducials)
+        self.add_sm_opening_button.clicked.connect(self.add_soldermask_opening)
+
+        self.fid_type_radio.activated_custom.connect(self.on_fiducial_type)
+        self.pos_radio.activated_custom.connect(self.on_second_point)
+        self.mode_radio.activated_custom.connect(self.on_method_change)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolFiducials()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            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, _("Fiducials Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+J', **kwargs)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units']
+        self.fid_size_entry.set_value(self.app.defaults["tools_fiducials_dia"])
+        self.margin_entry.set_value(float(self.app.defaults["tools_fiducials_margin"]))
+        self.mode_radio.set_value(self.app.defaults["tools_fiducials_mode"])
+        self.pos_radio.set_value(self.app.defaults["tools_fiducials_second_pos"])
+        self.fid_type_radio.set_value(self.app.defaults["tools_fiducials_type"])
+        self.line_thickness_entry.set_value(float(self.app.defaults["tools_fiducials_line_thickness"]))
+
+        self.click_points = list()
+        self.bottom_left_coords_entry.set_value('')
+        self.top_right_coords_entry.set_value('')
+        self.sec_points_coords_entry.set_value('')
+
+        self.copper_obj_set = set()
+        self.sm_obj_set = set()
+
+    def on_second_point(self, val):
+        if val == 'no':
+            self.id_item_3.setFlags(QtCore.Qt.NoItemFlags)
+            self.sec_point_coords_lbl.setFlags(QtCore.Qt.NoItemFlags)
+            self.sec_points_coords_entry.setDisabled(True)
+        else:
+            self.id_item_3.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.sec_points_coords_entry.setDisabled(False)
+
+    def on_method_change(self, val):
+        """
+        Make sure that on method change we disconnect the event handlers and reset the points storage
+        :param val: value of the Radio button which trigger this method
+        :return: None
+        """
+        if val == 'auto':
+            self.click_points = list()
+
+            try:
+                self.disconnect_event_handlers()
+            except TypeError:
+                pass
+
+    def on_fiducial_type(self, val):
+        if val == 'cross':
+            self.line_thickness_label.setDisabled(False)
+            self.line_thickness_entry.setDisabled(False)
+        else:
+            self.line_thickness_label.setDisabled(True)
+            self.line_thickness_entry.setDisabled(True)
+
+    def add_fiducials(self):
+        self.app.call_source = "fiducials_tool"
+        self.mode_method = self.mode_radio.get_value()
+        self.margin_val = self.margin_entry.get_value()
+        self.sec_position = self.pos_radio.get_value()
+        fid_type = self.fid_type_radio.get_value()
+
+        self.click_points = list()
+
+        # get the Gerber object on which the Fiducial will be inserted
+        selection_index = self.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolFiducials.execute() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        self.copper_obj_set.add(self.grb_object.options['name'])
+
+        if self.mode_method == 'auto':
+            xmin, ymin, xmax, ymax = self.grb_object.bounds()
+            bbox = box(xmin, ymin, xmax, ymax)
+            buf_bbox = bbox.buffer(self.margin_val, join_style=2)
+            x0, y0, x1, y1 = buf_bbox.bounds
+
+            self.click_points.append(
+                (
+                    float('%.*f' % (self.decimals, x0)),
+                    float('%.*f' % (self.decimals, y0))
+                )
+            )
+            self.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0))
+
+            self.click_points.append(
+                (
+                    float('%.*f' % (self.decimals, x1)),
+                    float('%.*f' % (self.decimals, y1))
+                )
+            )
+            self.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1))
+
+            if self.sec_position == 'up':
+                self.click_points.append(
+                    (
+                        float('%.*f' % (self.decimals, x0)),
+                        float('%.*f' % (self.decimals, y1))
+                    )
+                )
+                self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1))
+            elif self.sec_position == 'down':
+                self.click_points.append(
+                    (
+                        float('%.*f' % (self.decimals, x1)),
+                        float('%.*f' % (self.decimals, y0))
+                    )
+                )
+                self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0))
+
+            self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
+            self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
+                                                                 filename=None,
+                                                                 local_use=self.grb_object, use_thread=False)
+            self.on_exit()
+        else:
+            self.app.inform.emit(_("Click to add first Fiducial. Bottom Left..."))
+            self.bottom_left_coords_entry.set_value('')
+            self.top_right_coords_entry.set_value('')
+            self.sec_points_coords_entry.set_value('')
+
+            self.connect_event_handlers()
+
+        # To be called after clicking on the plot.
+
+    def add_fiducials_geo(self, points_list, g_obj, fid_size=None, fid_type=None, line_size=None):
+        """
+        Add geometry to the solid_geometry of the copper Gerber object
+        :param points_list: list of coordinates for the fiducials
+        :param g_obj: the Gerber object where to add the geometry
+        :param fid_size: the overall size of the fiducial or fiducial opening depending on the g_obj type
+        :param fid_type: the type of fiducial: circular or cross
+        :param line_size: the line thickenss when the fiducial type is cross
+        :return:
+        """
+        fid_size = self.fid_size_entry.get_value() if fid_size is None else fid_size
+        fid_type = 'circular' if fid_type is None else fid_type
+        line_thickness = self.line_thickness_entry.get_value() if line_size is None else line_size
+
+        radius = fid_size / 2.0
+
+        if fid_type == 'circular':
+            geo_list = [Point(pt).buffer(radius) for pt in points_list]
+
+            aperture_found = None
+            for ap_id, ap_val in g_obj.apertures.items():
+                if ap_val['type'] == 'C' and ap_val['size'] == fid_size:
+                    aperture_found = ap_id
+                    break
+
+            if aperture_found:
+                for geo in geo_list:
+                    dict_el = dict()
+                    dict_el['follow'] = geo.centroid
+                    dict_el['solid'] = geo
+                    g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
+            else:
+                ap_keys = list(g_obj.apertures.keys())
+                if ap_keys:
+                    new_apid = str(int(max(ap_keys)) + 1)
+                else:
+                    new_apid = '10'
+
+                g_obj.apertures[new_apid] = dict()
+                g_obj.apertures[new_apid]['type'] = 'C'
+                g_obj.apertures[new_apid]['size'] = fid_size
+                g_obj.apertures[new_apid]['geometry'] = list()
+
+                for geo in geo_list:
+                    dict_el = dict()
+                    dict_el['follow'] = geo.centroid
+                    dict_el['solid'] = geo
+                    g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
+
+            s_list = list()
+            if g_obj.solid_geometry:
+                try:
+                    for poly in g_obj.solid_geometry:
+                        s_list.append(poly)
+                except TypeError:
+                    s_list.append(g_obj.solid_geometry)
+
+            s_list += geo_list
+            g_obj.solid_geometry = MultiPolygon(s_list)
+        elif fid_type == 'cross':
+            geo_list = list()
+
+            for pt in points_list:
+                x = pt[0]
+                y = pt[1]
+                line_geo_hor = LineString([
+                    (x - radius + (line_thickness / 2.0), y), (x + radius - (line_thickness / 2.0), y)
+                ])
+                line_geo_vert = LineString([
+                    (x, y - radius + (line_thickness / 2.0)), (x, y + radius - (line_thickness / 2.0))
+                ])
+                geo_list.append([line_geo_hor, line_geo_vert])
+
+            aperture_found = None
+            for ap_id, ap_val in g_obj.apertures.items():
+                if ap_val['type'] == 'C' and ap_val['size'] == line_thickness:
+                    aperture_found = ap_id
+                    break
+
+            geo_buff_list = list()
+            if aperture_found:
+                for geo in geo_list:
+                    geo_buff_h = geo[0].buffer(line_thickness / 2.0)
+                    geo_buff_v = geo[1].buffer(line_thickness / 2.0)
+                    geo_buff_list.append(geo_buff_h)
+                    geo_buff_list.append(geo_buff_v)
+
+                    dict_el = dict()
+                    dict_el['follow'] = geo_buff_h.centroid
+                    dict_el['solid'] = geo_buff_h
+                    g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
+                    dict_el['follow'] = geo_buff_v.centroid
+                    dict_el['solid'] = geo_buff_v
+                    g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
+            else:
+                ap_keys = list(g_obj.apertures.keys())
+                if ap_keys:
+                    new_apid = str(int(max(ap_keys)) + 1)
+                else:
+                    new_apid = '10'
+
+                g_obj.apertures[new_apid] = dict()
+                g_obj.apertures[new_apid]['type'] = 'C'
+                g_obj.apertures[new_apid]['size'] = line_thickness
+                g_obj.apertures[new_apid]['geometry'] = list()
+
+                for geo in geo_list:
+                    geo_buff_h = geo[0].buffer(line_thickness / 2.0)
+                    geo_buff_v = geo[1].buffer(line_thickness / 2.0)
+                    geo_buff_list.append(geo_buff_h)
+                    geo_buff_list.append(geo_buff_v)
+
+                    dict_el = dict()
+                    dict_el['follow'] = geo_buff_h.centroid
+                    dict_el['solid'] = geo_buff_h
+                    g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
+                    dict_el['follow'] = geo_buff_v.centroid
+                    dict_el['solid'] = geo_buff_v
+                    g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
+
+            s_list = list()
+            if g_obj.solid_geometry:
+                try:
+                    for poly in g_obj.solid_geometry:
+                        s_list.append(poly)
+                except TypeError:
+                    s_list.append(g_obj.solid_geometry)
+
+            geo_buff_list = MultiPolygon(geo_buff_list)
+            geo_buff_list = geo_buff_list.buffer(0)
+            for poly in geo_buff_list:
+                s_list.append(poly)
+            g_obj.solid_geometry = MultiPolygon(s_list)
+        else:
+            # chess pattern fiducial type
+            geo_list = list()
+
+            def make_square_poly(center_pt, side_size):
+                half_s = side_size / 2
+                x_center = center_pt[0]
+                y_center = center_pt[1]
+
+                pt1 = (x_center - half_s, y_center - half_s)
+                pt2 = (x_center + half_s, y_center - half_s)
+                pt3 = (x_center + half_s, y_center + half_s)
+                pt4 = (x_center - half_s, y_center + half_s)
+
+                return Polygon([pt1, pt2, pt3, pt4, pt1])
+
+            for pt in points_list:
+                x = pt[0]
+                y = pt[1]
+                first_square = make_square_poly(center_pt=(x-fid_size/4, y+fid_size/4), side_size=fid_size/2)
+                second_square = make_square_poly(center_pt=(x+fid_size/4, y-fid_size/4), side_size=fid_size/2)
+                geo_list += [first_square, second_square]
+
+            aperture_found = None
+            new_ap_size = math.sqrt(fid_size**2 + fid_size**2)
+            for ap_id, ap_val in g_obj.apertures.items():
+                if ap_val['type'] == 'R' and \
+                        round(ap_val['size'], ndigits=self.decimals) == round(new_ap_size, ndigits=self.decimals):
+                    aperture_found = ap_id
+                    break
+
+            geo_buff_list = list()
+            if aperture_found:
+                for geo in geo_list:
+                    geo_buff_list.append(geo)
+
+                    dict_el = dict()
+                    dict_el['follow'] = geo.centroid
+                    dict_el['solid'] = geo
+                    g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
+            else:
+                ap_keys = list(g_obj.apertures.keys())
+                if ap_keys:
+                    new_apid = str(int(max(ap_keys)) + 1)
+                else:
+                    new_apid = '10'
+
+                g_obj.apertures[new_apid] = dict()
+                g_obj.apertures[new_apid]['type'] = 'R'
+                g_obj.apertures[new_apid]['size'] = new_ap_size
+                g_obj.apertures[new_apid]['width'] = fid_size
+                g_obj.apertures[new_apid]['height'] = fid_size
+                g_obj.apertures[new_apid]['geometry'] = list()
+
+                for geo in geo_list:
+                    geo_buff_list.append(geo)
+
+                    dict_el = dict()
+                    dict_el['follow'] = geo.centroid
+                    dict_el['solid'] = geo
+                    g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
+
+            s_list = list()
+            if g_obj.solid_geometry:
+                try:
+                    for poly in g_obj.solid_geometry:
+                        s_list.append(poly)
+                except TypeError:
+                    s_list.append(g_obj.solid_geometry)
+
+            for poly in geo_buff_list:
+                s_list.append(poly)
+            g_obj.solid_geometry = MultiPolygon(s_list)
+
+    def add_soldermask_opening(self):
+        sm_opening_dia = self.fid_size_entry.get_value() * 2.0
+
+        # get the Gerber object on which the Fiducial will be inserted
+        selection_index = self.sm_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.sm_object_combo.rootModelIndex())
+
+        try:
+            self.sm_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolFiducials.add_soldermask_opening() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        self.sm_obj_set.add(self.sm_object.options['name'])
+        self.add_fiducials_geo(self.click_points, g_obj=self.sm_object, fid_size=sm_opening_dia, fid_type='circular')
+
+        self.sm_object.source_file = self.app.export_gerber(obj_name=self.sm_object.options['name'], filename=None,
+                                                            local_use=self.sm_object, use_thread=False)
+        self.on_exit()
+
+    def on_mouse_release(self, event):
+        if event.button == 1:
+            if self.app.is_legacy is False:
+                event_pos = event.pos
+            else:
+                event_pos = (event.xdata, event.ydata)
+
+            pos_canvas = self.canvas.translate_coords(event_pos)
+            if self.app.grid_status():
+                pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+            else:
+                pos = (pos_canvas[0], pos_canvas[1])
+            click_pt = Point([pos[0], pos[1]])
+
+            self.click_points.append(
+                (
+                    float('%.*f' % (self.decimals, click_pt.x)),
+                    float('%.*f' % (self.decimals, click_pt.y))
+                )
+            )
+            self.check_points()
+
+    def check_points(self):
+        fid_type = self.fid_type_radio.get_value()
+
+        if len(self.click_points) == 1:
+            self.bottom_left_coords_entry.set_value(self.click_points[0])
+            self.app.inform.emit(_("Click to add the last fiducial. Top Right..."))
+
+        if self.sec_position != 'no':
+            if len(self.click_points) == 2:
+                self.top_right_coords_entry.set_value(self.click_points[1])
+                self.app.inform.emit(_("Click to add the second fiducial. Top Left or Bottom Right..."))
+            elif len(self.click_points) == 3:
+                self.sec_points_coords_entry.set_value(self.click_points[2])
+                self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
+                self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
+                self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
+                                                                     filename=None,
+                                                                     local_use=self.grb_object, use_thread=False)
+                self.on_exit()
+        else:
+            if len(self.click_points) == 2:
+                self.top_right_coords_entry.set_value(self.click_points[1])
+                self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
+                self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
+                self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
+                                                                     filename=None,
+                                                                     local_use=self.grb_object, use_thread=False)
+                self.on_exit()
+
+    def on_mouse_move(self, event):
+        pass
+
+    def replot(self, obj, run_thread=True):
+        def worker_task():
+            with self.app.proc_container.new('%s...' % _("Plotting")):
+                obj.plot()
+
+        if run_thread:
+            self.app.worker_task.emit({'fcn': worker_task, 'params': []})
+        else:
+            worker_task()
+
+    def on_exit(self):
+        # plot the object
+        for ob_name in self.copper_obj_set:
+            try:
+                copper_obj = self.app.collection.get_by_name(name=ob_name)
+                if len(self.copper_obj_set) > 1:
+                    self.replot(obj=copper_obj, run_thread=False)
+                else:
+                    self.replot(obj=copper_obj)
+            except (AttributeError, TypeError):
+                continue
+
+            # update the bounding box values
+            try:
+                a, b, c, d = copper_obj.bounds()
+                copper_obj.options['xmin'] = a
+                copper_obj.options['ymin'] = b
+                copper_obj.options['xmax'] = c
+                copper_obj.options['ymax'] = d
+            except Exception as e:
+                log.debug("ToolFiducials.on_exit() copper_obj bounds error --> %s" % str(e))
+
+        for ob_name in self.sm_obj_set:
+            try:
+                sm_obj = self.app.collection.get_by_name(name=ob_name)
+                if len(self.sm_obj_set) > 1:
+                    self.replot(obj=sm_obj, run_thread=False)
+                else:
+                    self.replot(obj=sm_obj)
+            except (AttributeError, TypeError):
+                continue
+
+            # update the bounding box values
+            try:
+                a, b, c, d = sm_obj.bounds()
+                sm_obj.options['xmin'] = a
+                sm_obj.options['ymin'] = b
+                sm_obj.options['xmax'] = c
+                sm_obj.options['ymax'] = d
+            except Exception as e:
+                log.debug("ToolFiducials.on_exit() sm_obj bounds error --> %s" % str(e))
+
+        # reset the variables
+        self.grb_object = None
+        self.sm_object = None
+
+        # Events ID
+        self.mr = None
+        # self.mm = None
+
+        # Mouse cursor positions
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+
+        self.disconnect_event_handlers()
+
+        self.app.call_source = "app"
+        self.app.inform.emit('[success] %s' % _("Fiducials Tool exit."))
+
+    def connect_event_handlers(self):
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            # self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+            # self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+
+        self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+        # self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+
+    def disconnect_event_handlers(self):
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+            # self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.mr)
+            # self.app.plotcanvas.graph_event_disconnect(self.mm)
+
+        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                              self.app.on_mouse_click_over_plot)
+        # self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+        #                                                       self.app.on_mouse_move_over_plot)
+        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                              self.app.on_mouse_click_release_over_plot)
+
+    def flatten(self, geometry):
+        """
+        Creates a list of non-iterable linear geometry objects.
+        :param geometry: Shapely type or list or list of list of such.
+
+        Results are placed in self.flat_geometry
+        """
+
+        # ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                if geo is not None:
+                    self.flatten(geometry=geo)
+
+        # ## Not iterable, do the actual indexing and add.
+        except TypeError:
+            self.flat_geometry.append(geometry)
+
+        return self.flat_geometry

+ 605 - 63
flatcamTools/ToolFilm.py

@@ -9,12 +9,23 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
 from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
-    OptionalHideInputSection, OptionalInputSection
+    OptionalHideInputSection, OptionalInputSection, FCComboBox
 
 
 from copy import deepcopy
 from copy import deepcopy
 import logging
 import logging
 from shapely.geometry import Polygon, MultiPolygon, Point
 from shapely.geometry import Polygon, MultiPolygon, Point
 
 
+from reportlab.graphics import renderPDF
+from reportlab.pdfgen import canvas
+from reportlab.graphics import renderPM
+from reportlab.lib.units import inch, mm
+from reportlab.lib.pagesizes import landscape, portrait, A4
+
+from svglib.svglib import svg2rlg
+from xml.dom.minidom import parseString as parse_xml_string
+from lxml import etree as ET
+from io import StringIO
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -33,7 +44,7 @@ class Film(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -168,6 +179,12 @@ class Film(FlatCAMTool):
 
 
         self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
         self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
                                                                    self.film_scaley_label,  self.film_scaley_entry])
                                                                    self.film_scaley_label,  self.film_scaley_entry])
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
         # Skew Geometry
         # Skew Geometry
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb.setToolTip(
         self.film_skew_cb.setToolTip(
@@ -179,7 +196,7 @@ class Film(FlatCAMTool):
             QCheckBox {font-weight: bold; color: black}
             QCheckBox {font-weight: bold; color: black}
             """
             """
         )
         )
-        grid0.addWidget(self.film_skew_cb, 9, 0, 1, 2)
+        grid0.addWidget(self.film_skew_cb, 10, 0, 1, 2)
 
 
         self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
         self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
         self.film_skewx_entry = FCDoubleSpinner()
         self.film_skewx_entry = FCDoubleSpinner()
@@ -187,8 +204,8 @@ class Film(FlatCAMTool):
         self.film_skewx_entry.set_precision(self.decimals)
         self.film_skewx_entry.set_precision(self.decimals)
         self.film_skewx_entry.setSingleStep(0.01)
         self.film_skewx_entry.setSingleStep(0.01)
 
 
-        grid0.addWidget(self.film_skewx_label, 10, 0)
-        grid0.addWidget(self.film_skewx_entry, 10, 1)
+        grid0.addWidget(self.film_skewx_label, 11, 0)
+        grid0.addWidget(self.film_skewx_entry, 11, 1)
 
 
         self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
         self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
         self.film_skewy_entry = FCDoubleSpinner()
         self.film_skewy_entry = FCDoubleSpinner()
@@ -196,8 +213,8 @@ class Film(FlatCAMTool):
         self.film_skewy_entry.set_precision(self.decimals)
         self.film_skewy_entry.set_precision(self.decimals)
         self.film_skewy_entry.setSingleStep(0.01)
         self.film_skewy_entry.setSingleStep(0.01)
 
 
-        grid0.addWidget(self.film_skewy_label, 11, 0)
-        grid0.addWidget(self.film_skewy_entry, 11, 1)
+        grid0.addWidget(self.film_skewy_label, 12, 0)
+        grid0.addWidget(self.film_skewy_entry, 12, 1)
 
 
         self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.film_skew_ref_label.setToolTip(
         self.film_skew_ref_label.setToolTip(
@@ -211,12 +228,18 @@ class Film(FlatCAMTool):
                                             orientation='vertical',
                                             orientation='vertical',
                                             stretch=False)
                                             stretch=False)
 
 
-        grid0.addWidget(self.film_skew_ref_label, 12, 0)
-        grid0.addWidget(self.film_skew_reference, 12, 1)
+        grid0.addWidget(self.film_skew_ref_label, 13, 0)
+        grid0.addWidget(self.film_skew_reference, 13, 1)
 
 
         self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
         self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
                                                                  self.film_skewy_label,  self.film_skewy_entry,
                                                                  self.film_skewy_label,  self.film_skewy_entry,
                                                                  self.film_skew_reference])
                                                                  self.film_skew_reference])
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line1, 14, 0, 1, 2)
+
         # Mirror Geometry
         # Mirror Geometry
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb.setToolTip(
         self.film_mirror_cb.setToolTip(
@@ -227,7 +250,7 @@ class Film(FlatCAMTool):
             QCheckBox {font-weight: bold; color: black}
             QCheckBox {font-weight: bold; color: black}
             """
             """
         )
         )
-        grid0.addWidget(self.film_mirror_cb, 13, 0, 1, 2)
+        grid0.addWidget(self.film_mirror_cb, 15, 0, 1, 2)
 
 
         self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
         self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
                                           {'label': _('X'), 'value': 'x'},
                                           {'label': _('X'), 'value': 'x'},
@@ -236,13 +259,20 @@ class Film(FlatCAMTool):
                                          stretch=False)
                                          stretch=False)
         self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
         self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
 
 
-        grid0.addWidget(self.film_mirror_axis_label, 14, 0)
-        grid0.addWidget(self.film_mirror_axis, 14, 1)
+        grid0.addWidget(self.film_mirror_axis_label, 16, 0)
+        grid0.addWidget(self.film_mirror_axis, 16, 1)
 
 
         self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
         self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
                                                [self.film_mirror_axis_label, self.film_mirror_axis])
                                                [self.film_mirror_axis_label, self.film_mirror_axis])
 
 
-        grid0.addWidget(QtWidgets.QLabel(''), 15, 0)
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line2, 17, 0, 1, 2)
+
+        self.film_param_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Parameters"))
+
+        grid0.addWidget(self.film_param_label, 18, 0, 1, 2)
 
 
         # Scale Stroke size
         # Scale Stroke size
         self.film_scale_stroke_entry = FCDoubleSpinner()
         self.film_scale_stroke_entry = FCDoubleSpinner()
@@ -256,10 +286,10 @@ class Film(FlatCAMTool):
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
               "therefore the fine features may be more affected by this parameter.")
               "therefore the fine features may be more affected by this parameter.")
         )
         )
-        grid0.addWidget(self.film_scale_stroke_label, 16, 0)
-        grid0.addWidget(self.film_scale_stroke_entry, 16, 1)
+        grid0.addWidget(self.film_scale_stroke_label, 19, 0)
+        grid0.addWidget(self.film_scale_stroke_entry, 19, 1)
 
 
-        grid0.addWidget(QtWidgets.QLabel(''), 17, 0)
+        grid0.addWidget(QtWidgets.QLabel(''), 20, 0)
 
 
         # Film Type
         # Film Type
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
@@ -274,8 +304,8 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "with white on a black canvas.\n"
               "The Film format is SVG.")
               "The Film format is SVG.")
         )
         )
-        grid0.addWidget(self.film_type_label, 18, 0)
-        grid0.addWidget(self.film_type, 18, 1)
+        grid0.addWidget(self.film_type_label, 21, 0)
+        grid0.addWidget(self.film_type, 21, 1)
 
 
         # Boundary for negative film generation
         # Boundary for negative film generation
         self.boundary_entry = FCDoubleSpinner()
         self.boundary_entry = FCDoubleSpinner()
@@ -294,8 +324,8 @@ class Film(FlatCAMTool):
               "white color like the rest and which may confound with the\n"
               "white color like the rest and which may confound with the\n"
               "surroundings if not for this border.")
               "surroundings if not for this border.")
         )
         )
-        grid0.addWidget(self.boundary_label, 19, 0)
-        grid0.addWidget(self.boundary_entry, 19, 1)
+        grid0.addWidget(self.boundary_label, 22, 0)
+        grid0.addWidget(self.boundary_entry, 22, 1)
 
 
         self.boundary_label.hide()
         self.boundary_label.hide()
         self.boundary_entry.hide()
         self.boundary_entry.hide()
@@ -305,7 +335,7 @@ class Film(FlatCAMTool):
         self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
         self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "when done manually."))
                                    "when done manually."))
-        grid0.addWidget(self.punch_cb, 20, 0, 1, 2)
+        grid0.addWidget(self.punch_cb, 23, 0, 1, 2)
 
 
         # this way I can hide/show the frame
         # this way I can hide/show the frame
         self.punch_frame = QtWidgets.QFrame()
         self.punch_frame = QtWidgets.QFrame()
@@ -359,21 +389,146 @@ class Film(FlatCAMTool):
         self.punch_size_label.hide()
         self.punch_size_label.hide()
         self.punch_size_spinner.hide()
         self.punch_size_spinner.hide()
 
 
-        # Buttons
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line3, 0, 0, 1, 2)
+
+        # File type
+        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
+                                         {'label': _('PNG'), 'value': 'png'},
+                                         {'label': _('PDF'), 'value': 'pdf'}
+                                         ], stretch=False)
+
+        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
+        self.file_type_label.setToolTip(
+            _("The file type of the saved film. Can be:\n"
+              "- 'SVG' -> open-source vectorial format\n"
+              "- 'PNG' -> raster image\n"
+              "- 'PDF' -> portable document format")
+        )
+        grid1.addWidget(self.file_type_label, 1, 0)
+        grid1.addWidget(self.file_type_radio, 1, 1)
+
+        # Page orientation
+        self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
+        self.orientation_label.setToolTip(_("Can be:\n"
+                                            "- Portrait\n"
+                                            "- Landscape"))
+
+        self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
+                                           {'label': _('Landscape'), 'value': 'l'},
+                                           ], stretch=False)
+
+        grid1.addWidget(self.orientation_label, 2, 0)
+        grid1.addWidget(self.orientation_radio, 2, 1)
+
+        # Page Size
+        self.pagesize_label = QtWidgets.QLabel('%s:' % _("Page Size"))
+        self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
+
+        self.pagesize_combo = FCComboBox()
+
+        self.pagesize = dict()
+        self.pagesize.update(
+            {
+                'Bounds': None,
+                'A0': (841*mm, 1189*mm),
+                'A1': (594*mm, 841*mm),
+                'A2': (420*mm, 594*mm),
+                'A3': (297*mm, 420*mm),
+                'A4': (210*mm, 297*mm),
+                'A5': (148*mm, 210*mm),
+                'A6': (105*mm, 148*mm),
+                'A7': (74*mm, 105*mm),
+                'A8': (52*mm, 74*mm),
+                'A9': (37*mm, 52*mm),
+                'A10': (26*mm, 37*mm),
+
+                'B0': (1000*mm, 1414*mm),
+                'B1': (707*mm, 1000*mm),
+                'B2': (500*mm, 707*mm),
+                'B3': (353*mm, 500*mm),
+                'B4': (250*mm, 353*mm),
+                'B5': (176*mm, 250*mm),
+                'B6': (125*mm, 176*mm),
+                'B7': (88*mm, 125*mm),
+                'B8': (62*mm, 88*mm),
+                'B9': (44*mm, 62*mm),
+                'B10': (31*mm, 44*mm),
+
+                'C0': (917*mm, 1297*mm),
+                'C1': (648*mm, 917*mm),
+                'C2': (458*mm, 648*mm),
+                'C3': (324*mm, 458*mm),
+                'C4': (229*mm, 324*mm),
+                'C5': (162*mm, 229*mm),
+                'C6': (114*mm, 162*mm),
+                'C7': (81*mm, 114*mm),
+                'C8': (57*mm, 81*mm),
+                'C9': (40*mm, 57*mm),
+                'C10': (28*mm, 40*mm),
+
+                # American paper sizes
+                'LETTER': (8.5*inch, 11*inch),
+                'LEGAL': (8.5*inch, 14*inch),
+                'ELEVENSEVENTEEN': (11*inch, 17*inch),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*inch, 8*inch),
+                'HALF_LETTER': (5.5*inch, 8*inch),
+                'GOV_LETTER': (8*inch, 10.5*inch),
+                'GOV_LEGAL': (8.5*inch, 13*inch),
+                'LEDGER': (17*inch, 11*inch),
+            }
+        )
+
+        page_size_list = list(self.pagesize.keys())
+        self.pagesize_combo.addItems(page_size_list)
+
+        grid1.addWidget(self.pagesize_label, 3, 0)
+        grid1.addWidget(self.pagesize_combo, 3, 1)
 
 
+        self.on_film_type(val='hide')
+
+        # Buttons
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button.setToolTip(
         self.film_object_button.setToolTip(
             _("Create a Film for the selected object, within\n"
             _("Create a Film for the selected object, within\n"
               "the specified box. Does not create a new \n "
               "the specified box. Does not create a new \n "
-              "FlatCAM object, but directly save it in SVG format\n"
-              "which can be opened with Inkscape.")
+              "FlatCAM object, but directly save it in the\n"
+              "selected format.")
         )
         )
-        hlay.addWidget(self.film_object_button)
+        self.film_object_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        grid1.addWidget(self.film_object_button, 4, 0, 1, 2)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        self.units = self.app.defaults['units']
+
         # ## Signals
         # ## Signals
         self.film_object_button.clicked.connect(self.on_film_creation)
         self.film_object_button.clicked.connect(self.on_film_creation)
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
@@ -381,6 +536,8 @@ class Film(FlatCAMTool):
 
 
         self.film_type.activated_custom.connect(self.on_film_type)
         self.film_type.activated_custom.connect(self.on_film_type)
         self.source_punch.activated_custom.connect(self.on_punch_source)
         self.source_punch.activated_custom.connect(self.on_punch_source)
+        self.file_type_radio.activated_custom.connect(self.on_file_type)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def on_type_obj_index_changed(self, index):
     def on_type_obj_index_changed(self, index):
         obj_type = self.tf_type_obj_combo.currentIndex()
         obj_type = self.tf_type_obj_combo.currentIndex()
@@ -449,6 +606,9 @@ class Film(FlatCAMTool):
         self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
         self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
         self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
         self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
         self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
         self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
+        self.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
+        self.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
+        self.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
 
 
     def on_film_type(self, val):
     def on_film_type(self, val):
         type_of_film = val
         type_of_film = val
@@ -463,6 +623,18 @@ class Film(FlatCAMTool):
             self.boundary_entry.hide()
             self.boundary_entry.hide()
             self.punch_cb.show()
             self.punch_cb.show()
 
 
+    def on_file_type(self, val):
+        if val == 'pdf':
+            self.orientation_label.show()
+            self.orientation_radio.show()
+            self.pagesize_label.show()
+            self.pagesize_combo.show()
+        else:
+            self.orientation_label.hide()
+            self.orientation_radio.hide()
+            self.pagesize_label.hide()
+            self.pagesize_combo.hide()
+
     def on_punch_source(self, val):
     def on_punch_source(self, val):
         if val == 'pad' and self.punch_cb.get_value():
         if val == 'pad' and self.punch_cb.get_value():
             self.punch_size_label.show()
             self.punch_size_label.show()
@@ -485,21 +657,21 @@ class Film(FlatCAMTool):
 
 
         try:
         try:
             name = self.tf_object_combo.currentText()
             name = self.tf_object_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
             return
             return
 
 
         try:
         try:
             boxname = self.tf_box_combo.currentText()
             boxname = self.tf_box_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
             return
 
 
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
-
         source = self.source_punch.get_value()
         source = self.source_punch.get_value()
+        file_type = self.file_type_radio.get_value()
 
 
         # #################################################################
         # #################################################################
         # ################ STARTING THE JOB ###############################
         # ################ STARTING THE JOB ###############################
@@ -510,13 +682,13 @@ class Film(FlatCAMTool):
         if self.film_type.get_value() == "pos":
         if self.film_type.get_value() == "pos":
 
 
             if self.punch_cb.get_value() is False:
             if self.punch_cb.get_value() is False:
-                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width)
+                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
             else:
             else:
-                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width)
+                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width, ftype=file_type)
         else:
         else:
-            self.generate_negative_film(name, boxname, factor=scale_stroke_width)
+            self.generate_negative_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
 
 
-    def generate_positive_normal_film(self, name, boxname, factor):
+    def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
 
 
         scale_factor_x = None
         scale_factor_x = None
@@ -541,29 +713,40 @@ class Film(FlatCAMTool):
         if self.film_mirror_cb.get_value():
         if self.film_mirror_cb.get_value():
             if self.film_mirror_axis.get_value() != 'none':
             if self.film_mirror_axis.get_value() != 'none':
                 mirror = self.film_mirror_axis.get_value()
                 mirror = self.film_mirror_axis.get_value()
+
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
+
         try:
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG positive"),
-                directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                caption=_("Export positive film"),
+                directory=self.app.get_last_save_folder() + '/' + name + '_film',
+                filter=filter_ext)
         except TypeError:
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export positive film"))
 
 
         filename = str(filename)
         filename = str(filename)
 
 
         if str(filename) == "":
         if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export positive film cancelled."))
             return
             return
         else:
         else:
-            self.app.export_svg_positive(name, boxname, filename,
-                                         scale_stroke_factor=factor,
-                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                         skew_reference=skew_reference,
-                                         mirror=mirror
-                                         )
+            self.export_positive(name, boxname, filename,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror, ftype=ftype
+                                 )
 
 
-    def generate_positive_punched_film(self, name, boxname, source, factor):
+    def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
 
 
         film_obj = self.app.collection.get_by_name(name)
         film_obj = self.app.collection.get_by_name(name)
 
 
@@ -572,7 +755,7 @@ class Film(FlatCAMTool):
 
 
             try:
             try:
                 exc_name = self.exc_combo.currentText()
                 exc_name = self.exc_combo.currentText()
-            except Exception as e:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                 return
                 return
@@ -587,7 +770,7 @@ class Film(FlatCAMTool):
             outname = name + "_punched"
             outname = name + "_punched"
             self.app.new_object('gerber', outname, init_func)
             self.app.new_object('gerber', outname, init_func)
 
 
-            self.generate_positive_normal_film(outname, boxname, factor=factor)
+            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
         else:
         else:
             log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
             log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
 
 
@@ -638,9 +821,9 @@ class Film(FlatCAMTool):
             outname = name + "_punched"
             outname = name + "_punched"
             self.app.new_object('gerber', outname, init_func)
             self.app.new_object('gerber', outname, init_func)
 
 
-            self.generate_positive_normal_film(outname, boxname, factor=factor)
+            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
 
 
-    def generate_negative_film(self, name, boxname, factor):
+    def generate_negative_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
 
 
         scale_factor_x = None
         scale_factor_x = None
@@ -671,27 +854,386 @@ class Film(FlatCAMTool):
         if border is None:
         if border is None:
             border = 0
             border = 0
 
 
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
+
         try:
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG negative"),
-                directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                caption=_("Export negative film"),
+                directory=self.app.get_last_save_folder() + '/' + name + '_film',
+                filter=filter_ext)
         except TypeError:
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export negative film"))
 
 
         filename = str(filename)
         filename = str(filename)
 
 
         if str(filename) == "":
         if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export negative film cancelled."))
             return
             return
         else:
         else:
-            self.app.export_svg_negative(name, boxname, filename, border,
-                                         scale_stroke_factor=factor,
-                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                         skew_reference=skew_reference,
-                                         mirror=mirror
-                                         )
+            self.export_negative(name, boxname, filename, border,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror, ftype=ftype
+                                 )
+
+    def export_negative(self, obj_name, box_name, filename, boundary,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,
+                        use_thread=True, ftype='svg'):
+        """
+        Exports a Geometry Object to an SVG file in negative.
+
+        :param obj_name: the name of the FlatCAM object to be saved as SVG
+        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename: Path to the SVG file to save to.
+        :param boundary: thickness of a black border to surround all the features
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x: factor to scale the svg geometry on the X axis
+        :param scale_factor_y: factor to scale the svg geometry on the Y axis
+        :param skew_factor_x: factor to skew the svg geometry on the X axis
+        :param skew_factor_y: factor to skew the svg geometry on the Y axis
+        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
+        those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param use_thread: if to be run in a separate thread; boolean
+        :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
+        :return:
+        """
+        self.app.report_usage("export_negative()")
+
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
+
+        self.app.log.debug("export_svg() negative")
+
+        try:
+            obj = self.app.collection.get_by_name(str(obj_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.app.collection.get_by_name(str(box_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
+
+        def make_negative_film():
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            uom = obj.units.lower()
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+            miny_rect = str(bounds[1] - boundary)
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width - wrong, we do when we have lines with certain width
+            # We set opacity to maximum
+            # We set the color to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', '#FFFFFF')
+                child.set('opacity', '1.0')
+                child.set('stroke', '#FFFFFF')
+
+            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
+            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
+            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
+
+            first_svg_elem_tag = 'rect'
+            first_svg_elem_attribs = {
+                'x': minx,
+                'y': miny_rect,
+                'width': svgwidth,
+                'height': svgheight,
+                'id': 'neg_rect',
+                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
+            }
+
+            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
+            exported_svg = ET.tostring(root)
+
+            svg_elem = svg_header + str(exported_svg) + svg_footer
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
+
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
+                    return 'fail'
+            else:
+                try:
+                    if self.units == 'INCH':
+                        unit = inch
+                    else:
+                        unit = mm
+
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+
+                    p_size = self.pagesize_combo.get_value()
+                    if p_size == 'Bounds':
+                        renderPDF.drawToFile(drawing, filename)
+                    else:
+                        if self.orientation_radio.get_value() == 'p':
+                            page_size = portrait(self.pagesize[p_size])
+                        else:
+                            page_size = landscape(self.pagesize[p_size])
+
+                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
+                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+                        renderPDF.draw(drawing, my_canvas, 0, 0)
+                        my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PDF output --> %s" % str(e))
+                    return 'fail'
+
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+
+            def job_thread_film(app_obj):
+                try:
+                    make_negative_film()
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_negative_film()
+
+    def export_positive(self, obj_name, box_name, filename,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,
+                        use_thread=True, ftype='svg'):
+        """
+        Exports a Geometry Object to an SVG file in positive black.
+
+        :param obj_name: the name of the FlatCAM object to be saved as SVG
+        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename: Path to the SVG file to save to.
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x: factor to scale the svg geometry on the X axis
+        :param scale_factor_y: factor to scale the svg geometry on the Y axis
+        :param skew_factor_x: factor to skew the svg geometry on the X axis
+        :param skew_factor_y: factor to skew the svg geometry on the Y axis
+        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
+        those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+
+        :param use_thread: if to be run in a separate thread; boolean
+        :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
+        :return:
+        """
+        self.app.report_usage("export_positive()")
+
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
+
+        self.app.log.debug("export_svg() black")
+
+        try:
+            obj = self.app.collection.get_by_name(str(obj_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.app.collection.get_by_name(str(box_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
+
+        def make_positive_film():
+            log.debug("FilmTool.export_positive().make_positive_film()")
+
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width
+            # We set opacity to maximum
+            # We set the colour to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', str(self.app.defaults['tools_film_color']))
+                child.set('opacity', '1.0')
+                child.set('stroke', str(self.app.defaults['tools_film_color']))
+
+            exported_svg = ET.tostring(root)
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            # This contain the measure units
+            uom = obj.units.lower()
+
+            # Define a boundary around SVG of about 1.0mm (~39mils)
+            if uom in "mm":
+                boundary = 1.0
+            else:
+                boundary = 0.0393701
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
+
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
+                    return 'fail'
+            else:
+                try:
+                    if self.units == 'INCH':
+                        unit = inch
+                    else:
+                        unit = mm
+
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+
+                    p_size = self.pagesize_combo.get_value()
+                    if p_size == 'Bounds':
+                        renderPDF.drawToFile(drawing, filename)
+                    else:
+                        if self.orientation_radio.get_value() == 'p':
+                            page_size = portrait(self.pagesize[p_size])
+                        else:
+                            page_size = landscape(self.pagesize[p_size])
+
+                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
+                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+                        renderPDF.draw(drawing, my_canvas, 0, 0)
+                        my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PDF output --> %s" % str(e))
+                    return 'fail'
+
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+
+            def job_thread_film(app_obj):
+                try:
+                    make_positive_film()
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_positive_film()
 
 
     def reset_fields(self):
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 78 - 3
flatcamTools/ToolImage.py

@@ -26,6 +26,9 @@ class ToolImage(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
+        self.app = app
+        self.decimals = self.app.decimals
+
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % _('Image to PCB'))
         title_label = QtWidgets.QLabel("%s" % _('Image to PCB'))
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
@@ -59,6 +62,7 @@ class ToolImage(FlatCAMTool):
 
 
         # DPI value of the imported image
         # DPI value of the imported image
         self.dpi_entry = FCSpinner()
         self.dpi_entry = FCSpinner()
+        self.dpi_entry.set_range(0, 99999)
         self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
         self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
         self.dpi_label.setToolTip(_("Specify a DPI value for the image.") )
         self.dpi_label.setToolTip(_("Specify a DPI value for the image.") )
         ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
         ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
@@ -145,8 +149,11 @@ class ToolImage(FlatCAMTool):
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        self.on_image_type(val=False)
+
         # ## Signals
         # ## Signals
         self.import_button.clicked.connect(self.on_file_importimage)
         self.import_button.clicked.connect(self.on_file_importimage)
+        self.image_type.activated_custom.connect(self.on_image_type)
 
 
     def run(self, toggle=True):
     def run(self, toggle=True):
         self.app.report_usage("ToolImage()")
         self.app.report_usage("ToolImage()")
@@ -187,6 +194,28 @@ class ToolImage(FlatCAMTool):
         self.mask_g_entry.set_value(250)
         self.mask_g_entry.set_value(250)
         self.mask_b_entry.set_value(250)
         self.mask_b_entry.set_value(250)
 
 
+    def on_image_type(self, val):
+        if val == 'color':
+            self.mask_r_label.setDisabled(False)
+            self.mask_r_entry.setDisabled(False)
+            self.mask_g_label.setDisabled(False)
+            self.mask_g_entry.setDisabled(False)
+            self.mask_b_label.setDisabled(False)
+            self.mask_b_entry.setDisabled(False)
+
+            self.mask_bw_label.setDisabled(True)
+            self.mask_bw_entry.setDisabled(True)
+        else:
+            self.mask_r_label.setDisabled(True)
+            self.mask_r_entry.setDisabled(True)
+            self.mask_g_label.setDisabled(True)
+            self.mask_g_entry.setDisabled(True)
+            self.mask_b_label.setDisabled(True)
+            self.mask_b_entry.setDisabled(True)
+
+            self.mask_bw_label.setDisabled(False)
+            self.mask_bw_entry.setDisabled(False)
+
     def on_file_importimage(self):
     def on_file_importimage(self):
         """
         """
         Callback for menu item File->Import IMAGE.
         Callback for menu item File->Import IMAGE.
@@ -194,7 +223,7 @@ class ToolImage(FlatCAMTool):
         :type type_of_obj: str
         :type type_of_obj: str
         :return: None
         :return: None
         """
         """
-        mask = []
+        mask = list()
         self.app.log.debug("on_file_importimage()")
         self.app.log.debug("on_file_importimage()")
 
 
         _filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
         _filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
@@ -218,6 +247,52 @@ class ToolImage(FlatCAMTool):
         if filename == "":
         if filename == "":
             self.app.inform.emit(_("Open cancelled."))
             self.app.inform.emit(_("Open cancelled."))
         else:
         else:
-            self.app.worker_task.emit({'fcn': self.app.import_image,
+            self.app.worker_task.emit({'fcn': self.import_image,
                                        'params': [filename, type_obj, dpi, mode, mask]})
                                        'params': [filename, type_obj, dpi, mode, mask]})
-            #  self.import_svg(filename, "geometry")
+
+    def import_image(self, filename, o_type='gerber', dpi=96, mode='black', mask=None, outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the SVG file.
+
+        :param filename: Path to the SVG file.
+        :param o_type: type of FlatCAM objeect
+        :param dpi: dot per inch
+        :param mode: black or color
+        :param mask: dictate the level of detail
+        :param outname: name for the resulting file
+        :return:
+        """
+
+        self.app.report_usage("import_image()")
+
+        if mask is None:
+            mask = [250, 250, 250, 250]
+
+        if o_type is None or o_type == "geometry":
+            obj_type = "geometry"
+        elif o_type == "gerber":
+            obj_type = o_type
+        else:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Not supported type is picked as parameter. "
+                                   "Only Geometry and Gerber are supported"))
+            return
+
+        def obj_init(geo_obj, app_obj):
+            geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
+            geo_obj.multigeo = False
+
+        with self.app.proc_container.new(_("Importing Image")) as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+            units = self.app.defaults['units']
+
+            self.app.new_object(obj_type, name, obj_init)
+
+            # Register recent file
+            self.app.file_opened.emit("image", filename)
+
+            # GUI feedback
+            self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))

+ 13 - 13
flatcamTools/ToolMove.py

@@ -30,6 +30,8 @@ class ToolMove(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
+        self.app = app
+        self.decimals = self.app.decimals
 
 
         self.layout.setContentsMargins(0, 0, 3, 0)
         self.layout.setContentsMargins(0, 0, 3, 0)
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
@@ -160,11 +162,12 @@ class ToolMove(FlatCAMTool):
 
 
                     def job_move(app_obj):
                     def job_move(app_obj):
                         with self.app.proc_container.new(_("Moving...")) as proc:
                         with self.app.proc_container.new(_("Moving...")) as proc:
-                            try:
-                                if not obj_list:
-                                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object(s) selected."))
-                                    return "fail"
 
 
+                            if not obj_list:
+                                app_obj.app.inform.emit('[WARNING_NOTCL] %s' % _("No object(s) selected."))
+                                return "fail"
+
+                            try:
                                 # remove any mark aperture shape that may be displayed
                                 # remove any mark aperture shape that may be displayed
                                 for sel_obj in obj_list:
                                 for sel_obj in obj_list:
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
@@ -173,10 +176,9 @@ class ToolMove(FlatCAMTool):
 
 
                                     try:
                                     try:
                                         sel_obj.replotApertures.emit()
                                         sel_obj.replotApertures.emit()
-                                    except Exception as e:
+                                    except Exception:
                                         pass
                                         pass
 
 
-                                for sel_obj in obj_list:
                                     # offset solid_geometry
                                     # offset solid_geometry
                                     sel_obj.offset((dx, dy))
                                     sel_obj.offset((dx, dy))
 
 
@@ -186,15 +188,13 @@ class ToolMove(FlatCAMTool):
                                     sel_obj.options['ymin'] = b
                                     sel_obj.options['ymin'] = b
                                     sel_obj.options['xmax'] = c
                                     sel_obj.options['xmax'] = c
                                     sel_obj.options['ymax'] = d
                                     sel_obj.options['ymax'] = d
-
-                                # time to plot the moved objects
-                                self.replot_signal.emit(obj_list)
                             except Exception as e:
                             except Exception as e:
-                                proc.done()
-                                self.app.inform.emit('[ERROR_NOTCL] %s --> %s' % ('ToolMove.on_left_click()', str(e)))
+                                log.debug('[ERROR_NOTCL] %s --> %s' % ('ToolMove.on_left_click()', str(e)))
                                 return "fail"
                                 return "fail"
 
 
-                        proc.done()
+                            # time to plot the moved objects
+                            app_obj.replot_signal.emit(obj_list)
+
                         # delete the selection bounding box
                         # delete the selection bounding box
                         self.delete_shape()
                         self.delete_shape()
                         self.app.inform.emit('[success] %s %s' %
                         self.app.inform.emit('[success] %s %s' %
@@ -314,7 +314,7 @@ class ToolMove(FlatCAMTool):
 
 
     def draw_shape(self, shape):
     def draw_shape(self, shape):
 
 
-        if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
+        if self.app.defaults['units'].upper() == 'MM':
             proc_shape = shape.buffer(-0.1)
             proc_shape = shape.buffer(-0.1)
             proc_shape = proc_shape.buffer(0.2)
             proc_shape = proc_shape.buffer(0.2)
         else:
         else:

+ 158 - 111
flatcamTools/ToolNonCopperClear.py

@@ -39,7 +39,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
         Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
@@ -153,8 +153,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "If it's not successful then the non-copper clearing will fail, too.\n"
               "If it's not successful then the non-copper clearing will fail, too.\n"
               "- Clear -> the regular non-copper clearing."))
               "- Clear -> the regular non-copper clearing."))
 
 
-        form = QtWidgets.QFormLayout()
-        self.tools_box.addLayout(form)
+        grid1 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
 
 
         # Milling Type Radio Button
         # Milling Type Radio Button
         self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
         self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
@@ -172,6 +174,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "- conventional / useful when there is no backlash compensation")
               "- conventional / useful when there is no backlash compensation")
         )
         )
 
 
+        grid1.addWidget(self.milling_type_label, 0, 0)
+        grid1.addWidget(self.milling_type_radio, 0, 1)
+
         # Tool order
         # Tool order
         self.ncc_order_label = QtWidgets.QLabel('<b>%s:</b>' % _('Tool order'))
         self.ncc_order_label = QtWidgets.QLabel('<b>%s:</b>' % _('Tool order'))
         self.ncc_order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
         self.ncc_order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
@@ -191,9 +196,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                           "WARNING: using rest machining will automatically set the order\n"
                                           "WARNING: using rest machining will automatically set the order\n"
                                           "in reverse and disable this control."))
                                           "in reverse and disable this control."))
 
 
-        form.addRow(self.milling_type_label, self.milling_type_radio)
-        form.addRow(self.ncc_order_label, self.ncc_order_radio)
-        form.addRow(QtWidgets.QLabel(''))
+        grid1.addWidget(self.ncc_order_label, 1, 0)
+        grid1.addWidget(self.ncc_order_radio, 1, 1)
+        grid1.addWidget(QtWidgets.QLabel(''), 2, 0)
 
 
         self.milling_type_label.hide()
         self.milling_type_label.hide()
         self.milling_type_radio.hide()
         self.milling_type_radio.hide()
@@ -202,7 +207,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # ############### Tool selection ##############################
         # ############### Tool selection ##############################
         # #############################################################
         # #############################################################
         self.tool_sel_label = QtWidgets.QLabel('<b>%s</b>' % _("Tool Selection"))
         self.tool_sel_label = QtWidgets.QLabel('<b>%s</b>' % _("Tool Selection"))
-        form.addRow(self.tool_sel_label)
+        grid1.addWidget(self.tool_sel_label, 3, 0, 1, 2)
 
 
         # Tool Type Radio Button
         # Tool Type Radio Button
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
@@ -219,17 +224,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "- 'V-shape'\n"
               "- 'V-shape'\n"
               "- Circular")
               "- Circular")
         )
         )
-        form.addRow(self.tool_type_label, self.tool_type_radio)
-
-        # ### Add a new Tool ####
-        self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
-        self.addtool_entry_lbl.setToolTip(
-            _("Diameter for the new tool to add in the Tool Table")
-        )
-        self.addtool_entry = FCDoubleSpinner()
-        self.addtool_entry.set_precision(self.decimals)
-
-        form.addRow(self.addtool_entry_lbl, self.addtool_entry)
+        grid1.addWidget(self.tool_type_label, 4, 0)
+        grid1.addWidget(self.tool_type_radio, 4, 1)
 
 
         # Tip Dia
         # Tip Dia
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
@@ -239,7 +235,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tipdia_entry.set_precision(self.decimals)
         self.tipdia_entry.set_precision(self.decimals)
         self.tipdia_entry.setSingleStep(0.1)
         self.tipdia_entry.setSingleStep(0.1)
 
 
-        form.addRow(self.tipdialabel, self.tipdia_entry)
+        grid1.addWidget(self.tipdialabel, 5, 0)
+        grid1.addWidget(self.tipdia_entry, 5, 1)
 
 
         # Tip Angle
         # Tip Angle
         self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
         self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
@@ -250,7 +247,38 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tipangle_entry.set_precision(self.decimals)
         self.tipangle_entry.set_precision(self.decimals)
         self.tipangle_entry.setSingleStep(5)
         self.tipangle_entry.setSingleStep(5)
 
 
-        form.addRow(self.tipanglelabel, self.tipangle_entry)
+        grid1.addWidget(self.tipanglelabel, 6, 0)
+        grid1.addWidget(self.tipangle_entry, 6, 1)
+
+        # Cut Z entry
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In FlatCAM units.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-99999, -0.00000000000001)
+
+        self.cutz_entry.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In FlatCAM units.")
+        )
+        grid1.addWidget(cutzlabel, 7, 0)
+        grid1.addWidget(self.cutz_entry, 7, 1)
+
+        # ### Tool Diameter ####
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
+        self.addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool to add in the Tool Table.\n"
+              "If the tool is V-shape type then this value is automatically\n"
+              "calculated from the other parameters.")
+        )
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
+
+        grid1.addWidget(self.addtool_entry_lbl, 8, 0)
+        grid1.addWidget(self.addtool_entry, 8, 1)
 
 
         grid2 = QtWidgets.QGridLayout()
         grid2 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid2)
         self.tools_box.addLayout(grid2)
@@ -287,40 +315,21 @@ class NonCopperClear(FlatCAMTool, Gerber):
         e_lab_1 = QtWidgets.QLabel('<b>%s:</b>' % _("Parameters"))
         e_lab_1 = QtWidgets.QLabel('<b>%s:</b>' % _("Parameters"))
         grid3.addWidget(e_lab_1, 0, 0)
         grid3.addWidget(e_lab_1, 0, 0)
 
 
-        # Cut Z entry
-        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
-        cutzlabel.setToolTip(
-           _("Depth of cut into material. Negative value.\n"
-             "In FlatCAM units.")
-        )
-        self.cutz_entry = FCDoubleSpinner()
-        self.cutz_entry.set_precision(self.decimals)
-        self.cutz_entry.set_range(-99999, -0.00000000000001)
-
-        self.cutz_entry.setToolTip(
-           _("Depth of cut into material. Negative value.\n"
-             "In FlatCAM units.")
-        )
-        grid3.addWidget(cutzlabel, 1, 0)
-        grid3.addWidget(self.cutz_entry, 1, 1)
-
         # Overlap Entry
         # Overlap Entry
         nccoverlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         nccoverlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         nccoverlabel.setToolTip(
         nccoverlabel.setToolTip(
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
-              "Example:\n"
-              "A value here of 0.25 means 25%% from the tool diameter found above.\n\n"
               "Adjust the value starting with lower values\n"
               "Adjust the value starting with lower values\n"
               "and increasing it if areas that should be cleared are still \n"
               "and increasing it if areas that should be cleared are still \n"
               "not cleared.\n"
               "not cleared.\n"
-              "Lower values = faster processing, faster execution on PCB.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "due of too many paths.")
               "due of too many paths.")
         )
         )
-        self.ncc_overlap_entry = FCDoubleSpinner()
-        self.ncc_overlap_entry.set_precision(3)
+        self.ncc_overlap_entry = FCDoubleSpinner(suffix='%')
+        self.ncc_overlap_entry.set_precision(self.decimals)
         self.ncc_overlap_entry.setWrapping(True)
         self.ncc_overlap_entry.setWrapping(True)
-        self.ncc_overlap_entry.setRange(0.000, 0.999)
+        self.ncc_overlap_entry.setRange(0.000, 99.9999)
         self.ncc_overlap_entry.setSingleStep(0.1)
         self.ncc_overlap_entry.setSingleStep(0.1)
         grid3.addWidget(nccoverlabel, 2, 0)
         grid3.addWidget(nccoverlabel, 2, 0)
         grid3.addWidget(self.ncc_overlap_entry, 2, 1)
         grid3.addWidget(self.ncc_overlap_entry, 2, 1)
@@ -410,7 +419,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_offset_spinner.set_precision(4)
         self.ncc_offset_spinner.set_precision(4)
         self.ncc_offset_spinner.setWrapping(True)
         self.ncc_offset_spinner.setWrapping(True)
 
 
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        units = self.app.defaults['units'].upper()
         if units == 'MM':
         if units == 'MM':
             self.ncc_offset_spinner.setSingleStep(0.1)
             self.ncc_offset_spinner.setSingleStep(0.1)
         else:
         else:
@@ -429,12 +438,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         ], orientation='vertical', stretch=False)
         ], orientation='vertical', stretch=False)
         self.reference_label = QtWidgets.QLabel(_("Reference:"))
         self.reference_label = QtWidgets.QLabel(_("Reference:"))
         self.reference_label.setToolTip(
         self.reference_label.setToolTip(
-            _("- 'Itself' -  the non copper clearing extent\n"
-              "is based on the object that is copper cleared.\n "
+            _("- 'Itself' - the non copper clearing extent is based on the object that is copper cleared.\n "
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
-              "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
-              "- 'Reference Object' -  will do non copper clearing within the area\n"
-              "specified by another object.")
+              "- 'Reference Object' - will do non copper clearing within the area specified by another object.")
         )
         )
         grid3.addWidget(self.reference_label, 10, 0)
         grid3.addWidget(self.reference_label, 10, 0)
         grid3.addWidget(self.reference_radio, 10, 1)
         grid3.addWidget(self.reference_radio, 10, 1)
@@ -448,9 +454,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "It can be Gerber, Excellon or Geometry.")
               "It can be Gerber, Excellon or Geometry.")
         )
         )
         self.box_combo_type = QtWidgets.QComboBox()
         self.box_combo_type = QtWidgets.QComboBox()
-        self.box_combo_type.addItem(_("Gerber   Reference Box Object"))
-        self.box_combo_type.addItem(_("Excellon Reference Box Object"))
-        self.box_combo_type.addItem(_("Geometry Reference Box Object"))
+        self.box_combo_type.addItem(_("Reference Gerber"))
+        self.box_combo_type.addItem(_("Reference Excellon"))
+        self.box_combo_type.addItem(_("Reference Geometry"))
         form1.addRow(self.box_combo_type_label, self.box_combo_type)
         form1.addRow(self.box_combo_type_label, self.box_combo_type)
 
 
         self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
         self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
@@ -473,8 +479,27 @@ class NonCopperClear(FlatCAMTool, Gerber):
             _("Create the Geometry Object\n"
             _("Create the Geometry Object\n"
               "for non-copper routing.")
               "for non-copper routing.")
         )
         )
+        self.generate_ncc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.generate_ncc_button)
         self.tools_box.addWidget(self.generate_ncc_button)
         self.tools_box.addStretch()
         self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.tools_box.addWidget(self.reset_button)
         # ############################ FINSIHED GUI ###################################
         # ############################ FINSIHED GUI ###################################
         # #############################################################################
         # #############################################################################
 
 
@@ -530,6 +555,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
 
 
+        self.tooldia = None
+
         # #############################################################################
         # #############################################################################
         # ############################ SGINALS ########################################
         # ############################ SGINALS ########################################
         # #############################################################################
         # #############################################################################
@@ -538,6 +565,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.generate_ncc_button.clicked.connect(self.on_ncc_click)
         self.generate_ncc_button.clicked.connect(self.on_ncc_click)
 
 
+        self.tipdia_entry.returnPressed.connect(self.on_calculate_tooldia)
+        self.tipangle_entry.returnPressed.connect(self.on_calculate_tooldia)
+        self.cutz_entry.returnPressed.connect(self.on_calculate_tooldia)
+
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.reference_radio.group_toggle_fn = self.on_toggle_reference
         self.reference_radio.group_toggle_fn = self.on_toggle_reference
         self.ncc_choice_offset_cb.stateChanged.connect(self.on_offset_choice)
         self.ncc_choice_offset_cb.stateChanged.connect(self.on_offset_choice)
@@ -545,6 +576,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_order_radio.activated_custom[str].connect(self.on_order_changed)
         self.ncc_order_radio.activated_custom[str].connect(self.on_order_changed)
 
 
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def on_type_obj_index_changed(self, index):
     def on_type_obj_index_changed(self, index):
         obj_type = self.type_obj_combo.currentIndex()
         obj_type = self.type_obj_combo.currentIndex()
@@ -554,7 +586,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
     def on_add_tool_by_key(self):
     def on_add_tool_by_key(self):
         tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
         tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
                                        text='%s:' % _('Enter a Tool Diameter'),
                                        text='%s:' % _('Enter a Tool Diameter'),
-                                       min=0.0000, max=99.9999, decimals=4)
+                                       min=0.0001, max=9999.9999, decimals=self.decimals)
         tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
         tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
 
 
         val, ok = tool_add_popup.get_value()
         val, ok = tool_add_popup.get_value()
@@ -605,7 +637,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
             self.decimals = 4
             self.decimals = 4
@@ -627,6 +659,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tool_type_radio.set_value(self.app.defaults["tools_ncctool_type"])
         self.tool_type_radio.set_value(self.app.defaults["tools_ncctool_type"])
         self.tipdia_entry.set_value(self.app.defaults["tools_ncctipdia"])
         self.tipdia_entry.set_value(self.app.defaults["tools_ncctipdia"])
         self.tipangle_entry.set_value(self.app.defaults["tools_ncctipangle"])
         self.tipangle_entry.set_value(self.app.defaults["tools_ncctipangle"])
+        self.addtool_entry.set_value(self.app.defaults["tools_nccnewdia"])
 
 
         self.on_tool_type(val=self.tool_type_radio.get_value())
         self.on_tool_type(val=self.tool_type_radio.get_value())
 
 
@@ -702,18 +735,13 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.bound_obj = None
         self.bound_obj = None
 
 
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
     def build_ui(self):
     def build_ui(self):
         self.ui_disconnect()
         self.ui_disconnect()
 
 
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        if self.units == "IN":
-            self.addtool_entry.set_value(0.039)
-        else:
-            self.addtool_entry.set_value(1)
+        self.units = self.app.defaults['units'].upper()
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.ncc_tools.items():
         for k, v in self.ncc_tools.items():
@@ -908,54 +936,50 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
     def on_tool_type(self, val):
     def on_tool_type(self, val):
         if val == 'V':
         if val == 'V':
-            self.addtool_entry_lbl.hide()
-            self.addtool_entry.hide()
+            self.addtool_entry_lbl.setDisabled(True)
+            self.addtool_entry.setDisabled(True)
             self.tipdialabel.show()
             self.tipdialabel.show()
             self.tipdia_entry.show()
             self.tipdia_entry.show()
             self.tipanglelabel.show()
             self.tipanglelabel.show()
             self.tipangle_entry.show()
             self.tipangle_entry.show()
         else:
         else:
-            self.addtool_entry_lbl.show()
-            self.addtool_entry.show()
+            self.addtool_entry_lbl.setDisabled(False)
+            self.addtool_entry.setDisabled(False)
             self.tipdialabel.hide()
             self.tipdialabel.hide()
             self.tipdia_entry.hide()
             self.tipdia_entry.hide()
             self.tipanglelabel.hide()
             self.tipanglelabel.hide()
             self.tipangle_entry.hide()
             self.tipangle_entry.hide()
 
 
-    def on_tool_add(self, dia=None, muted=None):
-
-        self.ui_disconnect()
+    def on_calculate_tooldia(self):
+        if self.tool_type_radio.get_value() == 'V':
+            tip_dia = float(self.tipdia_entry.get_value())
+            tip_angle = float(self.tipangle_entry.get_value()) / 2.0
+            cut_z = float(self.cutz_entry.get_value())
+            cut_z = -cut_z if cut_z < 0 else cut_z
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+            # calculated tool diameter so the cut_z parameter is obeyed
+            tool_dia = tip_dia + (2 * cut_z * math.tan(math.radians(tip_angle)))
 
 
-        if dia:
-            tool_dia = dia
-        else:
-            if self.tool_type_radio.get_value() == 'V':
+            # update the default_data so it is used in the ncc_tools dict
+            self.default_data.update({
+                "vtipdia": tip_dia,
+                "vtipangle": (tip_angle * 2),
+            })
 
 
-                tip_dia = float(self.tipdia_entry.get_value())
-                tip_angle = float(self.tipangle_entry.get_value()) / 2
-                cut_z = float(self.cutz_entry.get_value())
+            self.addtool_entry.set_value(tool_dia)
 
 
-                # calculated tool diameter so the cut_z parameter is obeyed
-                tool_dia = tip_dia + 2 * cut_z * math.tan(math.radians(tip_angle))
+            return tool_dia
+        else:
+            return float(self.addtool_entry.get_value())
 
 
-                # update the default_data so it is used in the ncc_tools dict
-                self.default_data.update({
-                    "vtipdia": tip_dia,
-                    "vtipangle": (tip_angle * 2),
-                })
-            else:
-                try:
-                    tool_dia = float(self.addtool_entry.get_value())
-                except ValueError:
-                    # try to convert comma to decimal point. if it's still not working error message and return
-                    try:
-                        tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
-                    except ValueError:
-                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-                        return
+    def on_tool_add(self, dia=None, muted=None):
+        self.ui_disconnect()
+        self.units = self.app.defaults['units'].upper()
 
 
+        if dia:
+            tool_dia = dia
+        else:
+            tool_dia = self.on_calculate_tooldia()
             if tool_dia is None:
             if tool_dia is None:
                 self.build_ui()
                 self.build_ui()
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter to add, in Float format."))
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter to add, in Float format."))
@@ -1114,15 +1138,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.reset_usage()
         self.reset_usage()
         self.app.report_usage("on_paint_button_click")
         self.app.report_usage("on_paint_button_click")
 
 
-        self.overlap = float(self.ncc_overlap_entry.get_value())
+        self.overlap = float(self.ncc_overlap_entry.get_value()) / 100.0
 
 
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
 
 
-        if self.overlap >= 1 or self.overlap < 0:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Overlap value must be between "
-                                                        "0 (inclusive) and 1 (exclusive), "))
-            return
-
         self.connect = self.ncc_connect_cb.get_value()
         self.connect = self.ncc_connect_cb.get_value()
         self.contour = self.ncc_contour_cb.get_value()
         self.contour = self.ncc_contour_cb.get_value()
         self.has_offset = self.ncc_choice_offset_cb.isChecked()
         self.has_offset = self.ncc_choice_offset_cb.isChecked()
@@ -1134,7 +1153,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
             self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
         except Exception as e:
         except Exception as e:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"),  str(self.obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"),  str(self.obj_name)))
-            return "Could not retrieve object: %s" % self.obj_name
+            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
 
 
         if self.ncc_obj is None:
         if self.ncc_obj is None:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
@@ -1174,8 +1193,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
             try:
             try:
                 self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name)
                 self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name)
             except Exception as e:
             except Exception as e:
-                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
-                return "Could not retrieve object: %s" % self.obj_name
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.bound_obj_name))
+                return "Could not retrieve object: %s with error: %s" % (self.bound_obj_name, str(e))
 
 
             self.clear_copper(ncc_obj=self.ncc_obj,
             self.clear_copper(ncc_obj=self.ncc_obj,
                               ncctooldia=self.ncc_dia_list,
                               ncctooldia=self.ncc_dia_list,
@@ -1258,13 +1277,20 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 pt3 = (x1, y1)
                 pt3 = (x1, y1)
                 pt4 = (x0, y1)
                 pt4 = (x0, y1)
 
 
-                self.sel_rect.append(Polygon([pt1, pt2, pt3, pt4]))
+                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                self.sel_rect.append(new_rectangle)
+
+                # add a temporary shape on canvas
+                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+
                 self.first_click = False
                 self.first_click = False
                 return
                 return
 
 
         elif event.button == right_button and self.mouse_is_dragging == False:
         elif event.button == right_button and self.mouse_is_dragging == False:
             self.first_click = False
             self.first_click = False
 
 
+            self.delete_tool_selection_shape()
+
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
@@ -1393,7 +1419,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # ####### Read the parameters #########################################
         # ####### Read the parameters #########################################
         # #####################################################################
         # #####################################################################
 
 
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value()
+        units = self.app.defaults['units']
 
 
         log.debug("NCC Tool started. Reading parameters.")
         log.debug("NCC Tool started. Reading parameters.")
         self.app.inform.emit(_("NCC Tool started. Reading parameters."))
         self.app.inform.emit(_("NCC Tool started. Reading parameters."))
@@ -1410,7 +1436,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         else:
         else:
             ncc_select = self.reference_radio.get_value()
             ncc_select = self.reference_radio.get_value()
 
 
-        overlap = overlap if overlap else float(self.app.defaults["tools_nccoverlap"])
+        overlap = overlap if overlap is not None else float(self.app.defaults["tools_nccoverlap"])
 
 
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
@@ -1598,11 +1624,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     sol_geo = ncc_obj.solid_geometry
                     sol_geo = ncc_obj.solid_geometry
 
 
                 if has_offset is True:
                 if has_offset is True:
-                    app_obj.inform.emit('[WARNING_NOTCL] %s ...' %
-                                        _("Buffering"))
+                    app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
-                    app_obj.inform.emit('[success] %s ...' %
-                                        _("Buffering finished"))
+                    app_obj.inform.emit('[success] %s ...' % _("Buffering finished"))
+
                 empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
                 empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
                 if empty == 'fail':
                 if empty == 'fail':
                     return 'fail'
                     return 'fail'
@@ -1769,7 +1794,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
                 # variables to display the percentage of work done
                 # variables to display the percentage of work done
                 geo_len = len(area.geoms)
                 geo_len = len(area.geoms)
-                disp_number = 0
+
                 old_disp_number = 0
                 old_disp_number = 0
                 log.warning("Total number of polygons to be cleared. %s" % str(geo_len))
                 log.warning("Total number of polygons to be cleared. %s" % str(geo_len))
 
 
@@ -1879,6 +1904,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 tools_storage.pop(k, None)
                 tools_storage.pop(k, None)
 
 
             geo_obj.options["cnctooldia"] = str(tool)
             geo_obj.options["cnctooldia"] = str(tool)
+
             geo_obj.multigeo = True
             geo_obj.multigeo = True
             geo_obj.tools.clear()
             geo_obj.tools.clear()
             geo_obj.tools = dict(tools_storage)
             geo_obj.tools = dict(tools_storage)
@@ -1895,6 +1921,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                                      "Change the painting parameters and try again."))
                                                      "Change the painting parameters and try again."))
                 return
                 return
 
 
+            # create the solid_geometry
+            geo_obj.solid_geometry = list()
+            for tooluid in geo_obj.tools:
+                if geo_obj.tools[tooluid]['solid_geometry']:
+                    try:
+                        for geo in geo_obj.tools[tooluid]['solid_geometry']:
+                            geo_obj.solid_geometry.append(geo)
+                    except TypeError:
+                        geo_obj.solid_geometry.append(geo_obj.tools[tooluid]['solid_geometry'])
+
             # Experimental...
             # Experimental...
             # print("Indexing...", end=' ')
             # print("Indexing...", end=' ')
             # geo_obj.make_index()
             # geo_obj.make_index()
@@ -2272,6 +2308,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
                         '[WARNING] %s: %s %s.' % (_("NCC Tool Rest Machining clear all done but the copper features "
                         '[WARNING] %s: %s %s.' % (_("NCC Tool Rest Machining clear all done but the copper features "
                                                     "isolation is broken for"), str(warning_flag), _("tools")))
                                                     "isolation is broken for"), str(warning_flag), _("tools")))
                 return
                 return
+
+                # create the solid_geometry
+                geo_obj.solid_geometry = list()
+                for tooluid in geo_obj.tools:
+                    if geo_obj.tools[tooluid]['solid_geometry']:
+                        try:
+                            for geo in geo_obj.tools[tooluid]['solid_geometry']:
+                                geo_obj.solid_geometry.append(geo)
+                        except TypeError:
+                            geo_obj.solid_geometry.append(geo_obj.tools[tooluid]['solid_geometry'])
             else:
             else:
                 # I will use this variable for this purpose although it was meant for something else
                 # I will use this variable for this purpose although it was meant for something else
                 # signal that we have no geo in the object therefore don't create it
                 # signal that we have no geo in the object therefore don't create it
@@ -2291,11 +2337,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 if run_threaded:
                 if run_threaded:
                     proc.done()
                     proc.done()
                 return
                 return
-            except Exception as e:
+            except Exception:
                 if run_threaded:
                 if run_threaded:
                     proc.done()
                     proc.done()
                 traceback.print_stack()
                 traceback.print_stack()
                 return
                 return
+
             if run_threaded:
             if run_threaded:
                 proc.done()
                 proc.done()
             else:
             else:

+ 36 - 4
flatcamTools/ToolOptimal.py

@@ -39,8 +39,8 @@ class ToolOptimal(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        self.decimals = 4
+        self.units = self.app.defaults['units'].upper()
+        self.decimals = self.app.decimals
 
 
         # ############################################################################
         # ############################################################################
         # ############################ GUI creation ##################################
         # ############################ GUI creation ##################################
@@ -116,6 +116,9 @@ class ToolOptimal(FlatCAMTool):
 
 
         # Locations where minimum was found
         # Locations where minimum was found
         self.locations_textb = FCTextArea(parent=self)
         self.locations_textb = FCTextArea(parent=self)
+        self.locations_textb.setPlaceholderText(
+            _("Coordinates for points where minimum distance was found.")
+        )
         self.locations_textb.setReadOnly(True)
         self.locations_textb.setReadOnly(True)
         stylesheet = """
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
                         QTextEdit { selection-background-color:blue;
@@ -164,6 +167,10 @@ class ToolOptimal(FlatCAMTool):
 
 
         # Other distances
         # Other distances
         self.distances_textb = FCTextArea(parent=self)
         self.distances_textb = FCTextArea(parent=self)
+        self.distances_textb.setPlaceholderText(
+            _("Other distances and the coordinates for points\n"
+              "where the distance was found.")
+        )
         self.distances_textb.setReadOnly(True)
         self.distances_textb.setReadOnly(True)
         stylesheet = """
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
                         QTextEdit { selection-background-color:blue;
@@ -184,6 +191,10 @@ class ToolOptimal(FlatCAMTool):
 
 
         # Locations where minimum was found
         # Locations where minimum was found
         self.locations_sec_textb = FCTextArea(parent=self)
         self.locations_sec_textb = FCTextArea(parent=self)
+        self.locations_sec_textb.setPlaceholderText(
+            _("Other distances and the coordinates for points\n"
+              "where the distance was found.")
+        )
         self.locations_sec_textb.setReadOnly(True)
         self.locations_sec_textb.setReadOnly(True)
         stylesheet = """
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
                         QTextEdit { selection-background-color:blue;
@@ -211,9 +222,30 @@ class ToolOptimal(FlatCAMTool):
               "this will allow the determination of the right tool to\n"
               "this will allow the determination of the right tool to\n"
               "use for isolation or copper clearing.")
               "use for isolation or copper clearing.")
         )
         )
+        self.calculate_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.calculate_button.setMinimumWidth(60)
         self.calculate_button.setMinimumWidth(60)
         self.layout.addWidget(self.calculate_button)
         self.layout.addWidget(self.calculate_button)
 
 
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
         self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
         self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
         self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
         self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
         # ################## Finished GUI creation ###################################
         # ################## Finished GUI creation ###################################
@@ -242,7 +274,7 @@ class ToolOptimal(FlatCAMTool):
         self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
         self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
         self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
         self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
 
 
-        self.layout.addStretch()
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
@@ -297,7 +329,7 @@ class ToolOptimal(FlatCAMTool):
         self.reset_fields()
         self.reset_fields()
 
 
     def find_minimum_distance(self):
     def find_minimum_distance(self):
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
         self.decimals = int(self.precision_spinner.get_value())
         self.decimals = int(self.precision_spinner.get_value())
 
 
         selection_index = self.gerber_object_combo.currentIndex()
         selection_index = self.gerber_object_combo.currentIndex()

+ 3 - 2
flatcamTools/ToolPDF.py

@@ -44,6 +44,7 @@ class ToolPDF(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         self.app = app
         self.app = app
+        self.decimals = self.app.decimals
         self.step_per_circles = self.app.defaults["gerber_circle_steps"]
         self.step_per_circles = self.app.defaults["gerber_circle_steps"]
 
 
         self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
         self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
@@ -134,7 +135,7 @@ class ToolPDF(FlatCAMTool):
         self.on_open_pdf_click()
         self.on_open_pdf_click()
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='CTRL+Q', **kwargs)
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
         pass
         pass
@@ -180,7 +181,7 @@ class ToolPDF(FlatCAMTool):
         self.pdf_decompressed[short_name] = ''
         self.pdf_decompressed[short_name] = ''
 
 
         # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
         # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
-        if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
+        if self.app.defaults['units'].upper() == 'MM':
             # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
             # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
             self.point_to_unit_factor = 25.4 / 72
             self.point_to_unit_factor = 25.4 / 72
         else:
         else:

+ 226 - 175
flatcamTools/ToolPaint.py

@@ -42,7 +42,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
         Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
@@ -219,19 +219,17 @@ class ToolPaint(FlatCAMTool, Gerber):
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         ovlabel.setToolTip(
         ovlabel.setToolTip(
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
-              "Example:\n"
-              "A value here of 0.25 means 25%% from the tool diameter found above.\n\n"
               "Adjust the value starting with lower values\n"
               "Adjust the value starting with lower values\n"
               "and increasing it if areas that should be painted are still \n"
               "and increasing it if areas that should be painted are still \n"
               "not painted.\n"
               "not painted.\n"
-              "Lower values = faster processing, faster execution on PCB.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "Higher values = slow processing and slow execution on CNC\n"
               "due of too many paths.")
               "due of too many paths.")
         )
         )
-        self.paintoverlap_entry = FCDoubleSpinner()
+        self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
         self.paintoverlap_entry.set_precision(3)
         self.paintoverlap_entry.set_precision(3)
         self.paintoverlap_entry.setWrapping(True)
         self.paintoverlap_entry.setWrapping(True)
-        self.paintoverlap_entry.setRange(0.000, 0.999)
+        self.paintoverlap_entry.setRange(0.0000, 99.9999)
         self.paintoverlap_entry.setSingleStep(0.1)
         self.paintoverlap_entry.setSingleStep(0.1)
         grid3.addWidget(ovlabel, 1, 0)
         grid3.addWidget(ovlabel, 1, 0)
         grid3.addWidget(self.paintoverlap_entry, 1, 1)
         grid3.addWidget(self.paintoverlap_entry, 1, 1)
@@ -301,27 +299,29 @@ class ToolPaint(FlatCAMTool, Gerber):
         # Polygon selection
         # Polygon selection
         selectlabel = QtWidgets.QLabel('%s:' % _('Selection'))
         selectlabel = QtWidgets.QLabel('%s:' % _('Selection'))
         selectlabel.setToolTip(
         selectlabel.setToolTip(
-            _("How to select Polygons to be painted.\n\n"
+            _("How to select Polygons to be painted.\n"
+              "- 'Polygon Selection' - left mouse click to add/remove polygons to be painted.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
               "- 'All Polygons' - the Paint will start after click.\n"
               "- 'All Polygons' - the Paint will start after click.\n"
-              "- 'Reference Object' -  will do non copper clearing within the area\n"
+              "- 'Reference Object' - will do non copper clearing within the area\n"
               "specified by another object.")
               "specified by another object.")
         )
         )
         grid3.addWidget(selectlabel, 7, 0)
         grid3.addWidget(selectlabel, 7, 0)
         # grid3 = QtWidgets.QGridLayout()
         # grid3 = QtWidgets.QGridLayout()
         self.selectmethod_combo = RadioSet([
         self.selectmethod_combo = RadioSet([
-            {"label": _("Single Polygon"), "value": "single"},
+            {"label": _("Polygon Selection"), "value": "single"},
             {"label": _("Area Selection"), "value": "area"},
             {"label": _("Area Selection"), "value": "area"},
             {"label": _("All Polygons"), "value": "all"},
             {"label": _("All Polygons"), "value": "all"},
             {"label": _("Reference Object"), "value": "ref"}
             {"label": _("Reference Object"), "value": "ref"}
         ], orientation='vertical', stretch=False)
         ], orientation='vertical', stretch=False)
         self.selectmethod_combo.setToolTip(
         self.selectmethod_combo.setToolTip(
-            _("How to select Polygons to be painted.\n\n"
+            _("How to select Polygons to be painted.\n"
+              "- 'Polygon Selection' - left mouse click to add/remove polygons to be painted.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be painted.\n"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
               "- 'All Polygons' - the Paint will start after click.\n"
               "- 'All Polygons' - the Paint will start after click.\n"
-              "- 'Reference Object' -  will do non copper clearing within the area\n"
+              "- 'Reference Object' - will do non copper clearing within the area\n"
               "specified by another object.")
               "specified by another object.")
         )
         )
         grid3.addWidget(self.selectmethod_combo, 7, 1)
         grid3.addWidget(self.selectmethod_combo, 7, 1)
@@ -335,9 +335,9 @@ class ToolPaint(FlatCAMTool, Gerber):
               "It can be Gerber, Excellon or Geometry.")
               "It can be Gerber, Excellon or Geometry.")
         )
         )
         self.box_combo_type = QtWidgets.QComboBox()
         self.box_combo_type = QtWidgets.QComboBox()
-        self.box_combo_type.addItem(_("Gerber   Reference Box Object"))
-        self.box_combo_type.addItem(_("Excellon Reference Box Object"))
-        self.box_combo_type.addItem(_("Geometry Reference Box Object"))
+        self.box_combo_type.addItem(_("Reference Gerber"))
+        self.box_combo_type.addItem(_("Reference Excellon"))
+        self.box_combo_type.addItem(_("Reference Geometry"))
         form1.addRow(self.box_combo_type_label, self.box_combo_type)
         form1.addRow(self.box_combo_type_label, self.box_combo_type)
 
 
         self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
         self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
@@ -364,9 +364,29 @@ class ToolPaint(FlatCAMTool, Gerber):
               "- 'Reference Object' -  will do non copper clearing within the area\n"
               "- 'Reference Object' -  will do non copper clearing within the area\n"
               "specified by another object.")
               "specified by another object.")
         )
         )
+        self.generate_paint_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.generate_paint_button)
         self.tools_box.addWidget(self.generate_paint_button)
 
 
         self.tools_box.addStretch()
         self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.tools_box.addWidget(self.reset_button)
+
         # #################################### FINSIHED GUI #####################################
         # #################################### FINSIHED GUI #####################################
         # #######################################################################################
         # #######################################################################################
 
 
@@ -381,6 +401,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.overlap = None
         self.overlap = None
         self.connect = None
         self.connect = None
         self.contour = None
         self.contour = None
+        self.select_method = None
 
 
         self.units = ''
         self.units = ''
         self.paint_tools = {}
         self.paint_tools = {}
@@ -395,6 +416,12 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         self.sel_rect = []
         self.sel_rect = []
 
 
+        # store here if the grid snapping is active
+        self.grid_status_memory = False
+
+        # dict to store the polygons selected for painting; key is the shape added to be plotted and value is the poly
+        self.poly_dict = dict()
+
         # store here the default data for Geometry Data
         # store here the default data for Geometry Data
         self.default_data = {}
         self.default_data = {}
         self.default_data.update({
         self.default_data.update({
@@ -446,6 +473,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
         # #############################################################################
         # #############################################################################
         # ###################### Setup CONTEXT MENU ###################################
         # ###################### Setup CONTEXT MENU ###################################
@@ -575,7 +603,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         # make the default object type, "Geometry"
         # make the default object type, "Geometry"
         self.type_obj_combo.setCurrentIndex(2)
         self.type_obj_combo.setCurrentIndex(2)
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
             self.decimals = 4
             self.decimals = 4
@@ -637,7 +665,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             pass
             pass
 
 
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.paint_tools.items():
         for k, v in self.paint_tools.items():
@@ -947,20 +975,11 @@ class ToolPaint(FlatCAMTool, Gerber):
         # #####################################################
         # #####################################################
         self.app.inform.emit(_("Paint Tool. Reading parameters."))
         self.app.inform.emit(_("Paint Tool. Reading parameters."))
 
 
-        self.overlap = float(self.paintoverlap_entry.get_value())
-
-        if self.overlap >= 1 or self.overlap < 0:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Overlap value must be between 0 (inclusive) and 1 (exclusive)"))
-            return
-
-        self.app.inform.emit('[WARNING_NOTCL] %s' %
-                             _("Click inside the desired polygon."))
+        self.overlap = float(self.paintoverlap_entry.get_value()) / 100.0
 
 
         self.connect = self.pathconnect_cb.get_value()
         self.connect = self.pathconnect_cb.get_value()
         self.contour = self.paintcontour_cb.get_value()
         self.contour = self.paintcontour_cb.get_value()
         self.select_method = self.selectmethod_combo.get_value()
         self.select_method = self.selectmethod_combo.get_value()
-
         self.obj_name = self.obj_combo.currentText()
         self.obj_name = self.obj_combo.currentText()
 
 
         # Get source object.
         # Get source object.
@@ -1004,8 +1023,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         continue
                         continue
                 self.tooldia_list.append(self.tooldia)
                 self.tooldia_list.append(self.tooldia)
         else:
         else:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
             return
             return
 
 
         if self.select_method == "all":
         if self.select_method == "all":
@@ -1017,36 +1035,16 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 contour=self.contour)
                                 contour=self.contour)
 
 
         elif self.select_method == "single":
         elif self.select_method == "single":
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Click inside the desired polygon."))
-
-            # use the first tool in the tool table; get the diameter
-            # tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text()))
-
-            # To be called after clicking on the plot.
-            def doit(event):
-                # do paint single only for left mouse clicks
-                if event.button == 1:
-                    self.app.inform.emit(_("Painting polygon..."))
-                    if self.app.is_legacy:
-                        self.app.plotcanvas.graph_event_disconnect('mouse_press', doit)
-                    else:
-                        self.app.plotcanvas.graph_event_disconnect(self.mp)
-
-                    pos = self.app.plotcanvas.translate_coords(event.pos)
-                    if self.app.grid_status() == True:
-                        pos = self.app.geo_editor.snap(pos[0], pos[1])
-
-                    self.paint_poly(self.paint_obj,
-                                    inside_pt=[pos[0], pos[1]],
-                                    tooldia=self.tooldia_list,
-                                    overlap=self.overlap,
-                                    connect=self.connect,
-                                    contour=self.contour)
-                    self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
-                                                                          self.app.on_mouse_click_over_plot)
-                    self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                                          self.app.on_mouse_click_release_over_plot)
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to paint it."))
+
+            # disengage the grid snapping since it may be hard to click on polygons with grid snapping on
+            if self.app.ui.grid_snap_btn.isChecked():
+                self.grid_status_memory = True
+                self.app.ui.grid_snap_btn.trigger()
+            else:
+                self.grid_status_memory = False
+
+            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_single_poly_mouse_release)
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
@@ -1054,11 +1052,9 @@ class ToolPaint(FlatCAMTool, Gerber):
             else:
             else:
                 self.app.plotcanvas.graph_event_disconnect(self.app.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.app.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.app.mp)
                 self.app.plotcanvas.graph_event_disconnect(self.app.mp)
-            self.mp = self.app.plotcanvas.graph_event_connect('mouse_press', doit)
 
 
         elif self.select_method == "area":
         elif self.select_method == "area":
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Click the start point of the paint area."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the paint area."))
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
                 self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
@@ -1071,7 +1067,6 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
-
         elif self.select_method == 'ref':
         elif self.select_method == 'ref':
             self.bound_obj_name = self.box_combo.currentText()
             self.bound_obj_name = self.box_combo.currentText()
             # Get source object.
             # Get source object.
@@ -1091,6 +1086,91 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 connect=self.connect,
                                 connect=self.connect,
                                 contour=self.contour)
                                 contour=self.contour)
 
 
+    # To be called after clicking on the plot.
+    def on_single_poly_mouse_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            right_button = 2
+            event_is_dragging = self.app.event_is_dragging
+        else:
+            event_pos = (event.xdata, event.ydata)
+            right_button = 3
+            event_is_dragging = self.app.ui.popMenu.mouse_is_panning
+
+        try:
+            x = float(event_pos[0])
+            y = float(event_pos[1])
+        except TypeError:
+            return
+
+        event_pos = (x, y)
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        # do paint single only for left mouse clicks
+        if event.button == 1:
+            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]), geoset=self.paint_obj.solid_geometry)
+
+            if clicked_poly:
+                if clicked_poly not in self.poly_dict.values():
+                    shape_id = self.app.tool_shapes.add(tolerance=self.paint_obj.drawing_tolerance,
+                                                        layer=0,
+                                                        shape=clicked_poly,
+                                                        color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                        face_color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                        visible=True)
+                    self.poly_dict[shape_id] = clicked_poly
+                    self.app.inform.emit(
+                        '%s: %d. %s' % (_("Added polygon"),
+                                        int(len(self.poly_dict)),
+                                        _("Click to add next polygon or right click to start painting."))
+                    )
+                else:
+                    try:
+                        for k, v in list(self.poly_dict.items()):
+                            if v == clicked_poly:
+                                self.app.tool_shapes.remove(k)
+                                self.poly_dict.pop(k)
+                                break
+                    except TypeError:
+                        return
+                    self.app.inform.emit(
+                        '%s. %s' % (_("Removed polygon"),
+                                    _("Click to add/remove next polygon or right click to start painting."))
+                    )
+
+                self.app.tool_shapes.redraw()
+            else:
+                self.app.inform.emit(_("No polygon detected under click position."))
+
+        elif event.button == right_button and event_is_dragging is False:
+            # restore the Grid snapping if it was active before
+            if self.grid_status_memory is True:
+                self.app.ui.grid_snap_btn.trigger()
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_single_poly_mouse_release)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            self.app.tool_shapes.clear(update=True)
+
+            if self.poly_dict:
+                poly_list = deepcopy(list(self.poly_dict.values()))
+                self.paint_poly(self.paint_obj,
+                                poly_list=poly_list,
+                                tooldia=self.tooldia_list,
+                                overlap=self.overlap,
+                                connect=self.connect,
+                                contour=self.contour)
+                self.poly_dict.clear()
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
+
     # To be called after clicking on the plot.
     # To be called after clicking on the plot.
     def on_mouse_release(self, event):
     def on_mouse_release(self, event):
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
@@ -1134,13 +1214,21 @@ class ToolPaint(FlatCAMTool, Gerber):
                 pt2 = (x1, y0)
                 pt2 = (x1, y0)
                 pt3 = (x1, y1)
                 pt3 = (x1, y1)
                 pt4 = (x0, y1)
                 pt4 = (x0, y1)
-                self.sel_rect.append(Polygon([pt1, pt2, pt3, pt4]))
+
+                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                self.sel_rect.append(new_rectangle)
+
+                # add a temporary shape on canvas
+                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+
                 self.first_click = False
                 self.first_click = False
                 return
                 return
 
 
         elif event.button == right_button and self.mouse_is_dragging is False:
         elif event.button == right_button and self.mouse_is_dragging is False:
             self.first_click = False
             self.first_click = False
 
 
+            self.delete_tool_selection_shape()
+
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
@@ -1220,6 +1308,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
     def paint_poly(self, obj,
     def paint_poly(self, obj,
                    inside_pt=None,
                    inside_pt=None,
+                   poly_list=None,
                    tooldia=None,
                    tooldia=None,
                    overlap=None,
                    overlap=None,
                    order=None,
                    order=None,
@@ -1251,19 +1340,15 @@ class ToolPaint(FlatCAMTool, Gerber):
         :return: None
         :return: None
         """
         """
 
 
-        # Which polygon.
-        # poly = find_polygon(self.solid_geometry, inside_pt)
         if isinstance(obj, FlatCAMGerber):
         if isinstance(obj, FlatCAMGerber):
             if self.app.defaults["gerber_buffering"] == 'no':
             if self.app.defaults["gerber_buffering"] == 'no':
                 self.app.inform.emit('%s %s %s' %
                 self.app.inform.emit('%s %s %s' %
                                      (_("Paint Tool."), _("Normal painting polygon task started."),
                                      (_("Paint Tool."), _("Normal painting polygon task started."),
                                       _("Buffering geometry...")))
                                       _("Buffering geometry...")))
             else:
             else:
-                self.app.inform.emit('%s %s' %
-                                     (_("Paint Tool."), _("Normal painting polygon task started.")))
+                self.app.inform.emit('%s %s' % (_("Paint Tool."), _("Normal painting polygon task started.")))
         else:
         else:
-            self.app.inform.emit('%s %s' %
-                                 (_("Paint Tool."), _("Normal painting polygon task started.")))
+            self.app.inform.emit('%s %s' % (_("Paint Tool."), _("Normal painting polygon task started.")))
 
 
         if isinstance(obj, FlatCAMGerber):
         if isinstance(obj, FlatCAMGerber):
             if self.app.defaults["tools_paint_plotting"] == 'progressive':
             if self.app.defaults["tools_paint_plotting"] == 'progressive':
@@ -1272,44 +1357,29 @@ class ToolPaint(FlatCAMTool, Gerber):
                 else:
                 else:
                     obj.solid_geometry = obj.solid_geometry.buffer(0)
                     obj.solid_geometry = obj.solid_geometry.buffer(0)
 
 
-        poly = self.find_polygon(point=inside_pt, geoset=obj.solid_geometry)
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
-
-        if margin is not None:
-            paint_margin = margin
-        else:
-            try:
-                paint_margin = float(self.paintmargin_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
-        # determine if to use the progressive plotting
-        if self.app.defaults["tools_paint_plotting"] == 'progressive':
-            prog_plot = True
-        else:
-            prog_plot = False
+        polygon_list = None
+        if inside_pt and poly_list is None:
+            polygon_list = [self.find_polygon(point=inside_pt, geoset=obj.solid_geometry)]
+        elif inside_pt is None and poly_list:
+            polygon_list = poly_list
 
 
         # No polygon?
         # No polygon?
-        if poly is None:
+        if polygon_list is None:
             self.app.log.warning('No polygon found.')
             self.app.log.warning('No polygon found.')
             self.app.inform.emit('[WARNING] %s' % _('No polygon found.'))
             self.app.inform.emit('[WARNING] %s' % _('No polygon found.'))
             return
             return
 
 
-        proc = self.app.proc_container.new(_("Painting polygon..."))
-        self.app.inform.emit('%s %s: %s' %
-                             (_("Paint Tool."), _("Painting polygon at location"), str(inside_pt)))
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
+        paint_margin = float(self.paintmargin_entry.get_value()) if margin is None else margin
+        # determine if to use the progressive plotting
+        prog_plot = True if self.app.defaults["tools_paint_plotting"] == 'progressive' else False
 
 
         name = outname if outname is not None else self.obj_name + "_paint"
         name = outname if outname is not None else self.obj_name + "_paint"
-
         over = overlap if overlap is not None else float(self.app.defaults["tools_paintoverlap"])
         over = overlap if overlap is not None else float(self.app.defaults["tools_paintoverlap"])
         conn = connect if connect is not None else self.app.defaults["tools_pathconnect"]
         conn = connect if connect is not None else self.app.defaults["tools_pathconnect"]
         cont = contour if contour is not None else self.app.defaults["tools_paintcontour"]
         cont = contour if contour is not None else self.app.defaults["tools_paintcontour"]
         order = order if order is not None else self.order_radio.get_value()
         order = order if order is not None else self.order_radio.get_value()
+        tools_storage = self.paint_tools if tools_storage is None else tools_storage
 
 
         sorted_tools = []
         sorted_tools = []
         if tooldia is not None:
         if tooldia is not None:
@@ -1324,24 +1394,17 @@ class ToolPaint(FlatCAMTool, Gerber):
             for row in range(self.tools_table.rowCount()):
             for row in range(self.tools_table.rowCount()):
                 sorted_tools.append(float(self.tools_table.item(row, 1).text()))
                 sorted_tools.append(float(self.tools_table.item(row, 1).text()))
 
 
-        if tools_storage is not None:
-            tools_storage = tools_storage
-        else:
-            tools_storage = self.paint_tools
+        # sort the tools if we have an order selected in the UI
+        if order == 'fwd':
+            sorted_tools.sort(reverse=False)
+        elif order == 'rev':
+            sorted_tools.sort(reverse=True)
+
+        proc = self.app.proc_container.new(_("Painting polygon..."))
 
 
         # Initializes the new geometry object
         # Initializes the new geometry object
         def gen_paintarea(geo_obj, app_obj):
         def gen_paintarea(geo_obj, app_obj):
-            # assert isinstance(geo_obj, FlatCAMGeometry), \
-            #     "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
-            # assert isinstance(app_obj, App)
-
-            tool_dia = None
-            if order == 'fwd':
-                sorted_tools.sort(reverse=False)
-            elif order == 'rev':
-                sorted_tools.sort(reverse=True)
-            else:
-                pass
+            geo_obj.solid_geometry = list()
 
 
             def paint_p(polyg, tooldiameter):
             def paint_p(polyg, tooldiameter):
                 cpoly = None
                 cpoly = None
@@ -1388,21 +1451,8 @@ class ToolPaint(FlatCAMTool, Gerber):
                                         _('Geometry could not be painted completely'))
                                         _('Geometry could not be painted completely'))
                     return None
                     return None
 
 
-            try:
-                a, b, c, d = poly.bounds
-                geo_obj.options['xmin'] = a
-                geo_obj.options['ymin'] = b
-                geo_obj.options['xmax'] = c
-                geo_obj.options['ymax'] = d
-            except Exception as e:
-                log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e))
-                return
-
-            total_geometry = []
             current_uid = int(1)
             current_uid = int(1)
-
-            geo_obj.solid_geometry = []
-
+            tool_dia = None
             for tool_dia in sorted_tools:
             for tool_dia in sorted_tools:
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 for k, v in tools_storage.items():
                 for k, v in tools_storage.items():
@@ -1410,68 +1460,77 @@ class ToolPaint(FlatCAMTool, Gerber):
                         current_uid = int(k)
                         current_uid = int(k)
                         break
                         break
 
 
+            try:
+                poly_buf = [pol.buffer(-paint_margin) for pol in polygon_list]
+                cp = list()
                 try:
                 try:
-                    poly_buf = poly.buffer(-paint_margin)
-                    if isinstance(poly_buf, MultiPolygon):
-                        cp = []
-                        for pp in poly_buf:
-                            cp.append(paint_p(pp, tooldia=tool_dia))
-                    else:
-                        cp = paint_p(poly_buf, tooldia=tool_dia)
+                    for pp in poly_buf:
+                        cp.append(paint_p(pp, tooldiameter=tool_dia))
+                except TypeError:
+                    cp = paint_p(poly_buf, tooldiameter=tool_dia)
 
 
-                    if cp is not None:
-                        if isinstance(cp, list):
-                            for x in cp:
-                                total_geometry += list(x.get_objects())
-                        else:
-                            total_geometry = list(cp.get_objects())
-                except FlatCAMApp.GracefulException:
-                    return "fail"
-                except Exception as e:
-                    log.debug("Could not Paint the polygons. %s" % str(e))
-                    app_obj.inform.emit('[ERROR] %s\n%s' %
-                                        (_("Could not do Paint. Try a different combination of parameters. "
-                                           "Or a different strategy of paint"),
-                                         str(e)
-                                         )
-                                        )
-                    return "fail"
+                total_geometry = list()
+                if cp:
+                    try:
+                        for x in cp:
+                            total_geometry += list(x.get_objects())
+                    except TypeError:
+                        total_geometry = list(cp.get_objects())
+            except FlatCAMApp.GracefulException:
+                return "fail"
+            except Exception as e:
+                log.debug("Could not Paint the polygons. %s" % str(e))
+                app_obj.inform.emit('[ERROR] %s\n%s' %
+                                    (_("Could not do Paint. Try a different combination of parameters. "
+                                       "Or a different strategy of paint"),
+                                     str(e)
+                                     )
+                                    )
+                return "fail"
 
 
-                # add the solid_geometry to the current too in self.paint_tools (tools_storage)
-                # dictionary and then reset the temporary list that stored that solid_geometry
-                tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry)
+            # add the solid_geometry to the current too in self.paint_tools (tools_storage)
+            # dictionary and then reset the temporary list that stored that solid_geometry
+            tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry)
 
 
-                tools_storage[current_uid]['data']['name'] = name
-                total_geometry[:] = []
+            tools_storage[current_uid]['data']['name'] = name
 
 
             # clean the progressive plotted shapes if it was used
             # clean the progressive plotted shapes if it was used
             if self.app.defaults["tools_paint_plotting"] == 'progressive':
             if self.app.defaults["tools_paint_plotting"] == 'progressive':
                 self.temp_shapes.clear(update=True)
                 self.temp_shapes.clear(update=True)
 
 
             # delete tools with empty geometry
             # delete tools with empty geometry
-            keys_to_delete = []
             # look for keys in the tools_storage dict that have 'solid_geometry' values empty
             # look for keys in the tools_storage dict that have 'solid_geometry' values empty
-            for uid in tools_storage:
+            for uid in list(tools_storage.keys()):
                 # if the solid_geometry (type=list) is empty
                 # if the solid_geometry (type=list) is empty
                 if not tools_storage[uid]['solid_geometry']:
                 if not tools_storage[uid]['solid_geometry']:
-                    keys_to_delete.append(uid)
-
-            # actual delete of keys from the tools_storage dict
-            for k in keys_to_delete:
-                tools_storage.pop(k, None)
+                    tools_storage.pop(uid, None)
 
 
             geo_obj.options["cnctooldia"] = str(tool_dia)
             geo_obj.options["cnctooldia"] = str(tool_dia)
-            # this turn on the FlatCAMCNCJob plot for multiple tools
+
+            # this will turn on the FlatCAMCNCJob plot for multiple tools
             geo_obj.multigeo = True
             geo_obj.multigeo = True
             geo_obj.multitool = True
             geo_obj.multitool = True
             geo_obj.tools.clear()
             geo_obj.tools.clear()
             geo_obj.tools = dict(tools_storage)
             geo_obj.tools = dict(tools_storage)
 
 
+            geo_obj.solid_geometry = cascaded_union(tools_storage[current_uid]['solid_geometry'])
+
+            try:
+                a, b, c, d = geo_obj.solid_geometry.bounds
+                geo_obj.options['xmin'] = a
+                geo_obj.options['ymin'] = b
+                geo_obj.options['xmax'] = c
+                geo_obj.options['ymax'] = d
+            except Exception as e:
+                log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e))
+                return
+
             # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
             # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
             has_solid_geo = 0
             has_solid_geo = 0
             for tooluid in geo_obj.tools:
             for tooluid in geo_obj.tools:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
                     has_solid_geo += 1
+
             if has_solid_geo == 0:
             if has_solid_geo == 0:
                 self.app.inform.emit('[ERROR] %s' %
                 self.app.inform.emit('[ERROR] %s' %
                                      _("There is no Painting Geometry in the file.\n"
                                      _("There is no Painting Geometry in the file.\n"
@@ -1479,6 +1538,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                        "Change the painting parameters and try again."))
                                        "Change the painting parameters and try again."))
                 return
                 return
 
 
+            total_geometry[:] = []
             self.app.inform.emit('[success] %s' % _("Paint Single Done."))
             self.app.inform.emit('[success] %s' % _("Paint Single Done."))
 
 
             # Experimental...
             # Experimental...
@@ -1550,21 +1610,12 @@ class ToolPaint(FlatCAMTool, Gerber):
         Usage of the different one is related to when this function is called from a TcL command.
         Usage of the different one is related to when this function is called from a TcL command.
         :return:
         :return:
         """
         """
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
 
 
         if margin is not None:
         if margin is not None:
             paint_margin = margin
             paint_margin = margin
         else:
         else:
-            try:
-                paint_margin = float(self.paintmargin_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
+            paint_margin = float(self.paintmargin_entry.get_value())
 
 
         # determine if to use the progressive plotting
         # determine if to use the progressive plotting
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
@@ -1979,7 +2030,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             except FlatCAMApp.GracefulException:
             except FlatCAMApp.GracefulException:
                 proc.done()
                 proc.done()
                 return
                 return
-            except Exception as e:
+            except Exception:
                 proc.done()
                 proc.done()
                 traceback.print_stack()
                 traceback.print_stack()
                 return
                 return
@@ -2027,7 +2078,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         Usage of the different one is related to when this function is called from a TcL command.
         Usage of the different one is related to when this function is called from a TcL command.
         :return:
         :return:
         """
         """
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
 
 
         if margin is not None:
         if margin is not None:
             paint_margin = margin
             paint_margin = margin
@@ -2454,7 +2505,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             except FlatCAMApp.GracefulException:
             except FlatCAMApp.GracefulException:
                 proc.done()
                 proc.done()
                 return
                 return
-            except Exception as e:
+            except Exception:
                 proc.done()
                 proc.done()
                 traceback.print_stack()
                 traceback.print_stack()
                 return
                 return

+ 24 - 7
flatcamTools/ToolPanelize.py

@@ -34,8 +34,9 @@ class Panelize(FlatCAMTool):
     toolName = _("Panelize PCB")
     toolName = _("Panelize PCB")
 
 
     def __init__(self, app):
     def __init__(self, app):
-        super(Panelize, self).__init__(self)
-        self.app = app
+        self.decimals = app.decimals
+
+        FlatCAMTool.__init__(self, app)
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -245,25 +246,41 @@ class Panelize(FlatCAMTool):
             self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
             self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
 
 
         # Buttons
         # Buttons
-        hlay_2 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay_2)
-
-        hlay_2.addStretch()
         self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
         self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
         self.panelize_object_button.setToolTip(
         self.panelize_object_button.setToolTip(
             _("Panelize the specified object around the specified box.\n"
             _("Panelize the specified object around the specified box.\n"
               "In other words it creates multiple copies of the source object,\n"
               "In other words it creates multiple copies of the source object,\n"
               "arranged in a 2D array of rows and columns.")
               "arranged in a 2D array of rows and columns.")
         )
         )
-        hlay_2.addWidget(self.panelize_object_button)
+        self.panelize_object_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.panelize_object_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
         # Signals
         # Signals
         self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
         self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
         self.panelize_object_button.clicked.connect(self.on_panelize)
         self.panelize_object_button.clicked.connect(self.on_panelize)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
         self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
         # list to hold the temporary objects
         # list to hold the temporary objects
         self.objs = []
         self.objs = []

+ 1 - 0
flatcamTools/ToolPcbWizard.py

@@ -34,6 +34,7 @@ class PcbWizard(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.app = app
         self.app = app
+        self.decimals = self.app.decimals
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
         title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))

+ 267 - 49
flatcamTools/ToolProperties.py

@@ -7,12 +7,13 @@
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
-from FlatCAMObj import FlatCAMCNCjob
 
 
 from shapely.geometry import MultiPolygon, Polygon
 from shapely.geometry import MultiPolygon, Polygon
 from shapely.ops import cascaded_union
 from shapely.ops import cascaded_union
 
 
 from copy import deepcopy
 from copy import deepcopy
+import math
+
 import logging
 import logging
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -28,13 +29,15 @@ log = logging.getLogger('base')
 class Properties(FlatCAMTool):
 class Properties(FlatCAMTool):
     toolName = _("Properties")
     toolName = _("Properties")
 
 
-    calculations_finished = QtCore.pyqtSignal(float, float, float, float, object)
+    calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
 
 
+        self.decimals = self.app.decimals
+
         # this way I can hide/show the frame
         # this way I can hide/show the frame
         self.properties_frame = QtWidgets.QFrame()
         self.properties_frame = QtWidgets.QFrame()
         self.properties_frame.setContentsMargins(0, 0, 0, 0)
         self.properties_frame.setContentsMargins(0, 0, 0, 0)
@@ -113,39 +116,57 @@ class Properties(FlatCAMTool):
     def properties(self):
     def properties(self):
         obj_list = self.app.collection.get_selected()
         obj_list = self.app.collection.get_selected()
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Properties Tool was not displayed. No object selected."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Properties Tool was not displayed. No object selected."))
             self.app.ui.notebook.setTabText(2, _("Tools"))
             self.app.ui.notebook.setTabText(2, _("Tools"))
             self.properties_frame.hide()
             self.properties_frame.hide()
             self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
             self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
             return
             return
+
+        # delete the selection shape, if any
+        try:
+            self.app.delete_selection_shape()
+        except Exception as e:
+            log.debug("ToolProperties.Properties.properties() --> %s" % str(e))
+
+        # populate the properties items
         for obj in obj_list:
         for obj in obj_list:
             self.addItems(obj)
             self.addItems(obj)
-            self.app.inform.emit('[success] %s' %
-                                 _("Object Properties are displayed."))
+            self.app.inform.emit('[success] %s' % _("Object Properties are displayed."))
         self.app.ui.notebook.setTabText(2, _("Properties Tool"))
         self.app.ui.notebook.setTabText(2, _("Properties Tool"))
 
 
     def addItems(self, obj):
     def addItems(self, obj):
         parent = self.treeWidget.invisibleRootItem()
         parent = self.treeWidget.invisibleRootItem()
         apertures = ''
         apertures = ''
         tools = ''
         tools = ''
+        drills = ''
+        slots = ''
+        others = ''
 
 
         font = QtGui.QFont()
         font = QtGui.QFont()
         font.setBold(True)
         font.setBold(True)
+
+        # main Items categories
         obj_type = self.addParent(parent, _('TYPE'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         obj_type = self.addParent(parent, _('TYPE'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         obj_name = self.addParent(parent, _('NAME'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         obj_name = self.addParent(parent, _('NAME'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         dims = self.addParent(parent, _('Dimensions'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         dims = self.addParent(parent, _('Dimensions'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         units = self.addParent(parent, _('Units'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         units = self.addParent(parent, _('Units'), expanded=True, color=QtGui.QColor("#000000"), font=font)
-
         options = self.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
         options = self.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
+
         if obj.kind.lower() == 'gerber':
         if obj.kind.lower() == 'gerber':
             apertures = self.addParent(parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
             apertures = self.addParent(parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         else:
         else:
             tools = self.addParent(parent, _('Tools'), expanded=True, color=QtGui.QColor("#000000"), font=font)
             tools = self.addParent(parent, _('Tools'), expanded=True, color=QtGui.QColor("#000000"), font=font)
 
 
+        if obj.kind.lower() == 'excellon':
+            drills = self.addParent(parent, _('Drills'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+            slots = self.addParent(parent, _('Slots'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+
+        if obj.kind.lower() == 'cncjob':
+            others = self.addParent(parent, _('Others'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+
         separator = self.addParent(parent, '')
         separator = self.addParent(parent, '')
 
 
-        self.addChild(obj_type, ['%s:' % _('Object Type'), ('%s' % (obj.kind.capitalize()))], True)
+        self.addChild(obj_type, ['%s:' % _('Object Type'), ('%s' % (obj.kind.upper()))], True, font=font, font_items=1)
         try:
         try:
             self.addChild(obj_type,
             self.addChild(obj_type,
                           ['%s:' % _('Geo Type'),
                           ['%s:' % _('Geo Type'),
@@ -162,6 +183,7 @@ class Properties(FlatCAMTool):
             length = 0.0
             length = 0.0
             width = 0.0
             width = 0.0
             area = 0.0
             area = 0.0
+            copper_area = 0.0
 
 
             geo = obj_prop.solid_geometry
             geo = obj_prop.solid_geometry
             if geo:
             if geo:
@@ -172,26 +194,56 @@ class Properties(FlatCAMTool):
                     length = abs(xmax - xmin)
                     length = abs(xmax - xmin)
                     width = abs(ymax - ymin)
                     width = abs(ymax - ymin)
                 except Exception as e:
                 except Exception as e:
-                    log.debug("PropertiesTool.addItems() --> %s" % str(e))
+                    log.debug("PropertiesTool.addItems() -> calculate dimensions --> %s" % str(e))
 
 
                 # calculate box area
                 # calculate box area
-                if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
+                if self.app.defaults['units'].lower() == 'mm':
                     area = (length * width) / 100
                     area = (length * width) / 100
                 else:
                 else:
                     area = length * width
                     area = length * width
+
+                if obj_prop.kind.lower() == 'gerber':
+                    # calculate copper area
+                    try:
+                        for geo_el in geo:
+                            copper_area += geo_el.area
+                    except TypeError:
+                        copper_area += geo.area
+                    copper_area /= 100
             else:
             else:
                 xmin = []
                 xmin = []
                 ymin = []
                 ymin = []
                 xmax = []
                 xmax = []
                 ymax = []
                 ymax = []
 
 
-                for tool_k in obj_prop.tools:
+                if obj_prop.kind.lower() == 'cncjob':
+                    try:
+                        for tool_k in obj_prop.exc_cnc_tools:
+                            x0, y0, x1, y1 = cascaded_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
+                    except Exception as ee:
+                        log.debug("PropertiesTool.addItems() --> %s" % str(ee))
+
+                    try:
+                        for tool_k in obj_prop.cnc_tools:
+                            x0, y0, x1, y1 = cascaded_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
+                    except Exception as ee:
+                        log.debug("PropertiesTool.addItems() --> %s" % str(ee))
+                else:
                     try:
                     try:
-                        x0, y0, x1, y1 = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
-                        xmin.append(x0)
-                        ymin.append(y0)
-                        xmax.append(x1)
-                        ymax.append(y1)
+                        for tool_k in obj_prop.tools:
+                            x0, y0, x1, y1 = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
                     except Exception as ee:
                     except Exception as ee:
                         log.debug("PropertiesTool.addItems() --> %s" % str(ee))
                         log.debug("PropertiesTool.addItems() --> %s" % str(ee))
 
 
@@ -205,15 +257,32 @@ class Properties(FlatCAMTool):
                     width = abs(ymax - ymin)
                     width = abs(ymax - ymin)
 
 
                     # calculate box area
                     # calculate box area
-                    if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
+                    if self.app.defaults['units'].lower() == 'mm':
                         area = (length * width) / 100
                         area = (length * width) / 100
                     else:
                     else:
                         area = length * width
                         area = length * width
+
+                    if obj_prop.kind.lower() == 'gerber':
+                        # calculate copper area
+
+                        # create a complete solid_geometry from the tools
+                        geo_tools = list()
+                        for tool_k in obj_prop.tools:
+                            if 'solid_geometry' in obj_prop.tools[tool_k]:
+                                for geo_el in obj_prop.tools[tool_k]['solid_geometry']:
+                                    geo_tools.append(geo_el)
+
+                        try:
+                            for geo_el in geo_tools:
+                                copper_area += geo_el.area
+                        except TypeError:
+                            copper_area += geo_tools.area
+                        copper_area /= 100
                 except Exception as e:
                 except Exception as e:
                     log.debug("Properties.addItems() --> %s" % str(e))
                     log.debug("Properties.addItems() --> %s" % str(e))
 
 
             area_chull = 0.0
             area_chull = 0.0
-            if not isinstance(obj_prop, FlatCAMCNCjob):
+            if obj_prop.kind.lower() != 'cncjob':
                 # calculate and add convex hull area
                 # calculate and add convex hull area
                 if geo:
                 if geo:
                     if isinstance(geo, MultiPolygon):
                     if isinstance(geo, MultiPolygon):
@@ -238,29 +307,35 @@ class Properties(FlatCAMTool):
                         area_chull = None
                         area_chull = None
                         log.debug("Properties.addItems() --> %s" % str(e))
                         log.debug("Properties.addItems() --> %s" % str(e))
 
 
-            if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
+            if self.app.defaults['units'].lower() == 'mm' and area_chull:
                 area_chull = area_chull / 100
                 area_chull = area_chull / 100
 
 
-            self.calculations_finished.emit(area, length, width, area_chull, dims)
+            if area_chull is None:
+                area_chull = 0
+
+            self.calculations_finished.emit(area, length, width, area_chull, copper_area, dims)
 
 
         self.app.worker_task.emit({'fcn': job_thread, 'params': [obj]})
         self.app.worker_task.emit({'fcn': job_thread, 'params': [obj]})
 
 
-        self.addChild(units,
-                      ['FlatCAM units:',
-                       {
-                           'in': _('Inch'),
-                           'mm': _('Metric')
-                       }
-                       [str(self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())]
-                       ],
-                      True
-                      )
+        # Units items
+        f_unit = {'in': _('Inch'), 'mm': _('Metric')}[str(self.app.defaults['units'].lower())]
+        self.addChild(units, ['FlatCAM units:', f_unit], True)
+
+        o_unit = {
+            'in': _('Inch'),
+            'mm': _('Metric'),
+            'inch': _('Inch'),
+            'metric': _('Metric')
+        }[str(obj.units_found.lower())]
+        self.addChild(units, ['Object units:', o_unit], True)
 
 
+        # Options items
         for option in obj.options:
         for option in obj.options:
             if option is 'name':
             if option is 'name':
                 continue
                 continue
             self.addChild(options, [str(option), str(obj.options[option])], True)
             self.addChild(options, [str(option), str(obj.options[option])], True)
 
 
+        # Items that depend on the object type
         if obj.kind.lower() == 'gerber':
         if obj.kind.lower() == 'gerber':
             temp_ap = dict()
             temp_ap = dict()
             for ap in obj.apertures:
             for ap in obj.apertures:
@@ -291,10 +366,43 @@ class Properties(FlatCAMTool):
                 apid = self.addParent(apertures, str(ap), expanded=False, color=QtGui.QColor("#000000"), font=font)
                 apid = self.addParent(apertures, str(ap), expanded=False, color=QtGui.QColor("#000000"), font=font)
                 for key in temp_ap:
                 for key in temp_ap:
                     self.addChild(apid, [str(key), str(temp_ap[key])], True)
                     self.addChild(apid, [str(key), str(temp_ap[key])], True)
-
         elif obj.kind.lower() == 'excellon':
         elif obj.kind.lower() == 'excellon':
+            tot_drill_cnt = 0
+            tot_slot_cnt = 0
+
             for tool, value in obj.tools.items():
             for tool, value in obj.tools.items():
-                self.addChild(tools, [str(tool), str(value['C'])], True)
+                toolid = self.addParent(tools, str(tool), expanded=False, color=QtGui.QColor("#000000"), font=font)
+
+                drill_cnt = 0  # variable to store the nr of drills per tool
+                slot_cnt = 0  # variable to store the nr of slots per tool
+
+                # Find no of drills for the current tool
+                for drill in obj.drills:
+                    if drill['tool'] == tool:
+                        drill_cnt += 1
+
+                tot_drill_cnt += drill_cnt
+
+                # Find no of slots for the current tool
+                for slot in obj.slots:
+                    if slot['tool'] == tool:
+                        slot_cnt += 1
+
+                tot_slot_cnt += slot_cnt
+
+                self.addChild(
+                    toolid,
+                    [
+                        _('Diameter'),
+                        '%.*f %s' % (self.decimals, value['C'], self.app.defaults['units'].lower())
+                    ],
+                    True
+                )
+                self.addChild(toolid, [_('Drills number'), str(drill_cnt)], True)
+                self.addChild(toolid, [_('Slots number'), str(slot_cnt)], True)
+
+            self.addChild(drills, [_('Drills total number:'), str(tot_drill_cnt)], True)
+            self.addChild(slots, [_('Slots total number:'), str(tot_slot_cnt)], True)
         elif obj.kind.lower() == 'geometry':
         elif obj.kind.lower() == 'geometry':
             for tool, value in obj.tools.items():
             for tool, value in obj.tools.items():
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
@@ -310,26 +418,110 @@ class Properties(FlatCAMTool):
                     else:
                     else:
                         self.addChild(geo_tool, [str(k), str(v)], True)
                         self.addChild(geo_tool, [str(k), str(v)], True)
         elif obj.kind.lower() == 'cncjob':
         elif obj.kind.lower() == 'cncjob':
+            # for cncjob objects made from gerber or geometry
             for tool, value in obj.cnc_tools.items():
             for tool, value in obj.cnc_tools.items():
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                 for k, v in value.items():
                     if k == 'solid_geometry':
                     if k == 'solid_geometry':
                         printed_value = _('Present') if v else _('None')
                         printed_value = _('Present') if v else _('None')
-                        self.addChild(geo_tool, [str(k), printed_value], True)
+                        self.addChild(geo_tool, [_("Solid Geometry"), printed_value], True)
                     elif k == 'gcode':
                     elif k == 'gcode':
                         printed_value = _('Present') if v != '' else _('None')
                         printed_value = _('Present') if v != '' else _('None')
-                        self.addChild(geo_tool, [str(k), printed_value], True)
+                        self.addChild(geo_tool, [_("GCode Text"), printed_value], True)
                     elif k == 'gcode_parsed':
                     elif k == 'gcode_parsed':
                         printed_value = _('Present') if v else _('None')
                         printed_value = _('Present') if v else _('None')
-                        self.addChild(geo_tool, [str(k), printed_value], True)
+                        self.addChild(geo_tool, [_("GCode Geometry"), printed_value], True)
                     elif k == 'data':
                     elif k == 'data':
-                        tool_data = self.addParent(geo_tool, str(k).capitalize(),
-                                                   color=QtGui.QColor("#000000"), font=font)
+                        tool_data = self.addParent(geo_tool, _("Data"), color=QtGui.QColor("#000000"), font=font)
                         for data_k, data_v in v.items():
                         for data_k, data_v in v.items():
-                            self.addChild(tool_data, [str(data_k), str(data_v)], True)
+                            self.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
                     else:
                     else:
                         self.addChild(geo_tool, [str(k), str(v)], True)
                         self.addChild(geo_tool, [str(k), str(v)], True)
 
 
+            # for cncjob objects made from excellon
+            for tool_dia, value in obj.exc_cnc_tools.items():
+                exc_tool = self.addParent(
+                    tools, str(value['tool']), expanded=False, color=QtGui.QColor("#000000"), font=font
+                )
+                self.addChild(
+                    exc_tool,
+                    [
+                        _('Diameter'),
+                        '%.*f %s' % (self.decimals, tool_dia, self.app.defaults['units'].lower())
+                    ],
+                    True
+                )
+                for k, v in value.items():
+                    if k == 'solid_geometry':
+                        printed_value = _('Present') if v else _('None')
+                        self.addChild(exc_tool, [_("Solid Geometry"), printed_value], True)
+                    elif k == 'nr_drills':
+                        self.addChild(exc_tool, [_("Drills number"), str(v)], True)
+                    elif k == 'nr_slots':
+                        self.addChild(exc_tool, [_("Slots number"), str(v)], True)
+                    else:
+                        pass
+
+                self.addChild(
+                    exc_tool,
+                    [
+                        _("Depth of Cut"),
+                        '%.*f %s' % (
+                            self.decimals,
+                            (obj.z_cut - obj.tool_offset[tool_dia]),
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+                self.addChild(
+                    exc_tool,
+                    [
+                        _("Clearance Height"),
+                        '%.*f %s' % (
+                            self.decimals,
+                            obj.z_move,
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+                self.addChild(
+                    exc_tool,
+                    [
+                        _("Feedrate"),
+                        '%.*f %s/min' % (
+                            self.decimals,
+                            obj.feedrate,
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+
+            r_time = obj.routing_time
+            if r_time > 1:
+                units_lbl = 'min'
+            else:
+                r_time *= 60
+                units_lbl = 'sec'
+            r_time = math.ceil(float(r_time))
+            self.addChild(
+                others,
+                [
+                    '%s:' % _('Routing time'),
+                    '%.*f %s' % (self.decimals, r_time, units_lbl)],
+                True
+            )
+            self.addChild(
+                others,
+                [
+                    '%s:' % _('Travelled distance'),
+                    '%.*f %s' % (self.decimals, obj.travel_distance, self.app.defaults['units'].lower())
+                ],
+                True
+            )
+
         self.addChild(separator, [''])
         self.addChild(separator, [''])
 
 
     def addParent(self, parent, title, expanded=False, color=None, font=None):
     def addParent(self, parent, title, expanded=False, color=None, font=None):
@@ -343,27 +535,53 @@ class Properties(FlatCAMTool):
             item.setFont(0, font)
             item.setFont(0, font)
         return item
         return item
 
 
-    def addChild(self, parent, title, column1=None):
+    def addChild(self, parent, title, column1=None, font=None, font_items=None):
         item = QtWidgets.QTreeWidgetItem(parent)
         item = QtWidgets.QTreeWidgetItem(parent)
         item.setText(0, str(title[0]))
         item.setText(0, str(title[0]))
         if column1 is not None:
         if column1 is not None:
             item.setText(1, str(title[1]))
             item.setText(1, str(title[1]))
+        if font and font_items:
+            try:
+                for fi in font_items:
+                    item.setFont(fi, font)
+            except TypeError:
+                item.setFont(font_items, font)
 
 
-    def show_area_chull(self, area, length, width, chull_area, location):
+    def show_area_chull(self, area, length, width, chull_area, copper_area, location):
 
 
         # add dimensions
         # add dimensions
-        self.addChild(location, ['%s:' % _('Length'), '%.4f %s' % (
-            length, self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())], True)
-        self.addChild(location, ['%s:' % _('Width'), '%.4f %s' % (
-            width, self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())], True)
+        self.addChild(
+            location,
+            ['%s:' % _('Length'), '%.*f %s' % (self.decimals, length, self.app.defaults['units'].lower())],
+            True
+        )
+        self.addChild(
+            location,
+            ['%s:' % _('Width'), '%.*f %s' % (self.decimals, width, self.app.defaults['units'].lower())],
+            True
+        )
 
 
         # add box area
         # add box area
-        if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
-            self.addChild(location, ['%s:' % _('Box Area'), '%.4f %s' % (area, 'cm2')], True)
-            self.addChild(location, ['%s:' % _('Convex_Hull Area'), '%.4f %s' % (chull_area, 'cm2')], True)
+        if self.app.defaults['units'].lower() == 'mm':
+            self.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'cm2')], True)
+            self.addChild(
+                location,
+                ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'cm2')],
+                True
+            )
 
 
         else:
         else:
-            self.addChild(location, ['%s:' % _('Box Area'), '%.4f %s' % (area, 'in2')], True)
-            self.addChild(location, ['%s:' % _('Convex_Hull Area'), '%.4f %s' % (chull_area, 'in2')], True)
+            self.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'in2')], True)
+            self.addChild(
+                location,
+                ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'in2')],
+                True
+            )
+
+        # add copper area
+        if self.app.defaults['units'].lower() == 'mm':
+            self.addChild(location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'cm2')], True)
+        else:
+            self.addChild(location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'in2')], True)
 
 
 # end of file
 # end of file

+ 886 - 0
flatcamTools/ToolQRCode.py

@@ -0,0 +1,886 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 10/24/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import Qt
+
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox
+from flatcamParsers.ParseSVG import *
+
+from shapely.geometry.base import *
+from shapely.ops import unary_union
+from shapely.affinity import translate
+from shapely.geometry import box
+
+from io import StringIO, BytesIO
+from collections import Iterable
+import logging
+from copy import deepcopy
+
+import qrcode
+import qrcode.image.svg
+import qrcode.image.pil
+from lxml import etree as ET
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class QRCode(FlatCAMTool):
+
+    toolName = _("QRCode Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+
+        self.decimals = self.app.decimals
+        self.units = ''
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        # ## Grid Layout
+        i_grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(i_grid_lay)
+        i_grid_lay.setColumnStretch(0, 0)
+        i_grid_lay.setColumnStretch(1, 1)
+
+        self.grb_object_combo = QtWidgets.QComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.setCurrentIndex(1)
+
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which the QRCode will be added.")
+        )
+
+        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
+        i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('QRCode Parameters'))
+        self.qrcode_label.setToolTip(
+            _("The parameters used to shape the QRCode.")
+        )
+        grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
+
+        # VERSION #
+        self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
+        self.version_label.setToolTip(
+            _("QRCode version can have values from 1 (21x21 boxes)\n"
+              "to 40 (177x177 boxes).")
+        )
+        self.version_entry = FCSpinner()
+        self.version_entry.set_range(1, 40)
+        self.version_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.version_label, 1, 0)
+        grid_lay.addWidget(self.version_entry, 1, 1)
+
+        # ERROR CORRECTION #
+        self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
+        self.error_label.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
+                                     {'label': 'M', 'value': 'M'},
+                                     {'label': 'Q', 'value': 'Q'},
+                                     {'label': 'H', 'value': 'H'}])
+        self.error_radio.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        grid_lay.addWidget(self.error_label, 2, 0)
+        grid_lay.addWidget(self.error_radio, 2, 1)
+
+        # BOX SIZE #
+        self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
+        self.bsize_label.setToolTip(
+            _("Box size control the overall size of the QRcode\n"
+              "by adjusting the size of each box in the code.")
+        )
+        self.bsize_entry = FCSpinner()
+        self.bsize_entry.set_range(1, 9999)
+        self.bsize_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.bsize_label, 3, 0)
+        grid_lay.addWidget(self.bsize_entry, 3, 1)
+
+        # BORDER SIZE #
+        self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
+        self.border_size_label.setToolTip(
+            _("Size of the QRCode border. How many boxes thick is the border.\n"
+              "Default value is 4. The width of the clearance around the QRCode.")
+        )
+        self.border_size_entry = FCSpinner()
+        self.border_size_entry.set_range(1, 9999)
+        self.border_size_entry.setWrapping(True)
+        self.border_size_entry.set_value(4)
+
+        grid_lay.addWidget(self.border_size_label, 4, 0)
+        grid_lay.addWidget(self.border_size_entry, 4, 1)
+
+        # Text box
+        self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
+        self.text_label.setToolTip(
+            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
+        )
+        self.text_data = FCTextArea()
+        self.text_data.setPlaceholderText(
+            _("Add here the text to be included in the QRCode...")
+        )
+        grid_lay.addWidget(self.text_label, 5, 0)
+        grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
+
+        # POLARITY CHOICE #
+        self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
+        self.pol_label.setToolTip(
+            _("Choose the polarity of the QRCode.\n"
+              "It can be drawn in a negative way (squares are clear)\n"
+              "or in a positive way (squares are opaque).")
+        )
+        self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
+                                   {'label': _('Positive'), 'value': 'pos'}])
+        self.pol_radio.setToolTip(
+            _("Choose the type of QRCode to be created.\n"
+              "If added on a Silkscreen Gerber file the QRCode may\n"
+              "be added as positive. If it is added to a Copper Gerber\n"
+              "file then perhaps the QRCode can be added as negative.")
+        )
+        grid_lay.addWidget(self.pol_label, 7, 0)
+        grid_lay.addWidget(self.pol_radio, 7, 1)
+
+        # BOUNDING BOX TYPE #
+        self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
+        self.bb_label.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
+                                  {'label': _('Square'), 'value': 's'}])
+        self.bb_radio.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        grid_lay.addWidget(self.bb_label, 8, 0)
+        grid_lay.addWidget(self.bb_radio, 8, 1)
+
+        # Export QRCode
+        self.export_cb = FCCheckBox(_("Export QRCode"))
+        self.export_cb.setToolTip(
+            _("Show a set of controls allowing to export the QRCode\n"
+              "to a SVG file or an PNG file.")
+        )
+        grid_lay.addWidget(self.export_cb, 9, 0, 1, 2)
+
+        # this way I can hide/show the frame
+        self.export_frame = QtWidgets.QFrame()
+        self.export_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.export_frame)
+        self.export_lay = QtWidgets.QGridLayout()
+        self.export_lay.setContentsMargins(0, 0, 0, 0)
+        self.export_frame.setLayout(self.export_lay)
+        self.export_lay.setColumnStretch(0, 0)
+        self.export_lay.setColumnStretch(1, 1)
+
+        # default is hidden
+        self.export_frame.hide()
+
+        # FILL COLOR #
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
+        self.fill_color_label.setToolTip(
+            _("Set the QRCode fill color (squares color).")
+        )
+        self.fill_color_entry = FCEntry()
+        self.fill_color_button = QtWidgets.QPushButton()
+        self.fill_color_button.setFixedSize(15, 15)
+
+        fill_lay_child = QtWidgets.QHBoxLayout()
+        fill_lay_child.setContentsMargins(0, 0, 0, 0)
+        fill_lay_child.addWidget(self.fill_color_entry)
+        fill_lay_child.addWidget(self.fill_color_button, alignment=Qt.AlignRight)
+        fill_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        fill_color_widget = QtWidgets.QWidget()
+        fill_color_widget.setLayout(fill_lay_child)
+
+        self.export_lay.addWidget(self.fill_color_label, 0, 0)
+        self.export_lay.addWidget(fill_color_widget, 0, 1)
+
+        self.transparent_cb = FCCheckBox(_("Transparent back color"))
+        self.export_lay.addWidget(self.transparent_cb, 1, 0, 1, 2)
+
+        # BACK COLOR #
+        self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
+        self.back_color_label.setToolTip(
+            _("Set the QRCode background color.")
+        )
+        self.back_color_entry = FCEntry()
+        self.back_color_button = QtWidgets.QPushButton()
+        self.back_color_button.setFixedSize(15, 15)
+
+        back_lay_child = QtWidgets.QHBoxLayout()
+        back_lay_child.setContentsMargins(0, 0, 0, 0)
+        back_lay_child.addWidget(self.back_color_entry)
+        back_lay_child.addWidget(self.back_color_button, alignment=Qt.AlignRight)
+        back_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        back_color_widget = QtWidgets.QWidget()
+        back_color_widget.setLayout(back_lay_child)
+
+        self.export_lay.addWidget(self.back_color_label, 2, 0)
+        self.export_lay.addWidget(back_color_widget, 2, 1)
+
+        # ## Export QRCode as SVG image
+        self.export_svg_button = QtWidgets.QPushButton(_("Export QRCode SVG"))
+        self.export_svg_button.setToolTip(
+            _("Export a SVG file with the QRCode content.")
+        )
+        self.export_svg_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2)
+
+        # ## Export QRCode as PNG image
+        self.export_png_button = QtWidgets.QPushButton(_("Export QRCode PNG"))
+        self.export_png_button.setToolTip(
+            _("Export a PNG image file with the QRCode content.")
+        )
+        self.export_png_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2)
+
+        # ## Insert QRCode
+        self.qrcode_button = QtWidgets.QPushButton(_("Insert QRCode"))
+        self.qrcode_button.setToolTip(
+            _("Create the QRCode object.")
+        )
+        self.qrcode_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.qrcode_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        self.grb_object = None
+        self.box_poly = None
+        self.proc = None
+
+        self.origin = (0, 0)
+
+        self.mm = None
+        self.mr = None
+        self.kr = None
+
+        self.shapes = self.app.move_tool.sel_shapes
+        self.qrcode_geometry = MultiPolygon()
+        self.qrcode_utility_geometry = MultiPolygon()
+
+        self.old_back_color = ''
+
+        # Signals #
+        self.qrcode_button.clicked.connect(self.execute)
+        self.export_cb.stateChanged.connect(self.on_export_frame)
+        self.export_png_button.clicked.connect(self.export_png_file)
+        self.export_svg_button.clicked.connect(self.export_svg_file)
+
+        self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
+        self.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
+        self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
+        self.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
+
+        self.transparent_cb.stateChanged.connect(self.on_transparent_back_color)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+    def run(self, toggle=True):
+        self.app.report_usage("QRCode()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            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, _("QRCode Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units']
+        self.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"]))
+        self.error_radio.set_value(self.app.defaults["tools_qrcode_error"])
+        self.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"]))
+        self.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"]))
+        self.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"])
+        self.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"])
+
+        self.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"])
+
+        self.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color'])
+        self.fill_color_button.setStyleSheet("background-color:%s" %
+                                             str(self.app.defaults['tools_qrcode_fill_color'])[:7])
+
+        self.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color'])
+        self.back_color_button.setStyleSheet("background-color:%s" %
+                                             str(self.app.defaults['tools_qrcode_back_color'])[:7])
+
+    def on_export_frame(self, state):
+        self.export_frame.setVisible(state)
+        self.qrcode_button.setVisible(not state)
+
+    def execute(self):
+        text_data = self.text_data.get_value()
+        if text_data == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
+            return 'fail'
+
+        # get the Gerber object on which the QRCode will be inserted
+        selection_index = self.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("QRCode.execute() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return 'fail'
+
+        # we can safely activate the mouse events
+        self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+        self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+        self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
+
+        self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry"))
+
+        def job_thread_qr(app_obj):
+            error_code = {
+                'L': qrcode.constants.ERROR_CORRECT_L,
+                'M': qrcode.constants.ERROR_CORRECT_M,
+                'Q': qrcode.constants.ERROR_CORRECT_Q,
+                'H': qrcode.constants.ERROR_CORRECT_H
+            }[self.error_radio.get_value()]
+
+            qr = qrcode.QRCode(
+                version=self.version_entry.get_value(),
+                error_correction=error_code,
+                box_size=self.bsize_entry.get_value(),
+                border=self.border_size_entry.get_value(),
+                image_factory=qrcode.image.svg.SvgFragmentImage
+            )
+            qr.add_data(text_data)
+            qr.make()
+
+            svg_file = BytesIO()
+            img = qr.make_image()
+            img.save(svg_file)
+
+            svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
+            svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
+            self.qrcode_geometry = deepcopy(svg_geometry)
+
+            svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
+            self.qrcode_utility_geometry = svg_geometry
+
+            # make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too
+            # complicated
+            try:
+                a, b, c, d = self.qrcode_utility_geometry.bounds
+                self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d)
+            except Exception as ee:
+                log.debug("QRCode.make() bounds error --> %s" % str(ee))
+
+            app_obj.call_source = 'qrcode_tool'
+            app_obj.inform.emit(_("Click on the Destination point ..."))
+
+        self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]})
+
+    def make(self, pos):
+        self.on_exit()
+
+        # make sure that the source object solid geometry is an Iterable
+        if not isinstance(self.grb_object.solid_geometry, Iterable):
+            self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
+
+        # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered
+        geo_list = self.grb_object.solid_geometry
+        if isinstance(self.grb_object.solid_geometry, MultiPolygon):
+            geo_list = list(self.grb_object.solid_geometry.geoms)
+
+        # this is the bounding box of the QRCode geometry
+        a, b, c, d = self.qrcode_utility_geometry.bounds
+        buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
+
+        if self.bb_radio.get_value() == 'r':
+            mask_geo = box(a, b, c, d).buffer(buff_val)
+        else:
+            mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
+
+        # update the solid geometry with the cutout (if it is the case)
+        new_solid_geometry = list()
+        offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1])
+        for poly in geo_list:
+            if poly.contains(offset_mask_geo):
+                new_solid_geometry.append(poly.difference(offset_mask_geo))
+            else:
+                if poly not in new_solid_geometry:
+                    new_solid_geometry.append(poly)
+
+        geo_list = deepcopy(list(new_solid_geometry))
+
+        # Polarity
+        if self.pol_radio.get_value() == 'pos':
+            working_geo = self.qrcode_utility_geometry
+        else:
+            working_geo = mask_geo.difference(self.qrcode_utility_geometry)
+
+        try:
+            for geo in working_geo:
+                geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
+        except TypeError:
+            geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1]))
+
+        self.grb_object.solid_geometry = deepcopy(geo_list)
+
+        box_size = float(self.bsize_entry.get_value()) / 10.0
+
+        sort_apid = list()
+        new_apid = '10'
+        if self.grb_object.apertures:
+            for k, v in list(self.grb_object.apertures.items()):
+                sort_apid.append(int(k))
+            sorted_apertures = sorted(sort_apid)
+            max_apid = max(sorted_apertures)
+            if max_apid >= 10:
+                new_apid = str(max_apid + 1)
+            else:
+                new_apid = '10'
+
+        # don't know if the condition is required since I already made sure above that the new_apid is a new one
+        if new_apid not in self.grb_object.apertures:
+            self.grb_object.apertures[new_apid] = dict()
+            self.grb_object.apertures[new_apid]['geometry'] = list()
+            self.grb_object.apertures[new_apid]['type'] = 'R'
+            # TODO: HACK
+            # I've artificially added 1% to the height and width because otherwise after loading the
+            # exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of
+            # one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion.
+            self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01)
+            self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01)
+            self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
+
+        if '0' not in self.grb_object.apertures:
+            self.grb_object.apertures['0'] = dict()
+            self.grb_object.apertures['0']['geometry'] = list()
+            self.grb_object.apertures['0']['type'] = 'REG'
+            self.grb_object.apertures['0']['size'] = 0.0
+
+        # in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture)
+        # make sure that I place a cutout there
+        zero_elem = dict()
+        zero_elem['clear'] = offset_mask_geo
+        self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem))
+
+        try:
+            a, b, c, d = self.grb_object.bounds()
+            self.grb_object.options['xmin'] = a
+            self.grb_object.options['ymin'] = b
+            self.grb_object.options['xmax'] = c
+            self.grb_object.options['ymax'] = d
+        except Exception as e:
+            log.debug("QRCode.make() bounds error --> %s" % str(e))
+
+        try:
+            for geo in self.qrcode_geometry:
+                geo_elem = dict()
+                geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1])
+                geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1])
+                self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
+        except TypeError:
+            geo_elem = dict()
+            geo_elem['solid'] = self.qrcode_geometry
+            self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
+
+        # update the source file with the new geometry:
+        self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
+                                                             local_use=self.grb_object, use_thread=False)
+
+        self.replot(obj=self.grb_object)
+        self.app.inform.emit('[success] %s' % _("QRCode Tool done."))
+
+    def draw_utility_geo(self, pos):
+
+        # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
+        outline = '#0000FFAF'
+
+        offset_geo = list()
+
+        # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is
+        # better seen in this (bit what if the sel.qrcode_geometry is just one geo element? len will fail ...
+        if len(self.qrcode_geometry) <= self.app.defaults["tools_qrcode_sel_limit"]:
+            try:
+                for poly in self.qrcode_utility_geometry:
+                    offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
+                    for geo_int in poly.interiors:
+                        offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
+            except TypeError:
+                offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
+                for geo_int in self.qrcode_utility_geometry.interiors:
+                    offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
+        else:
+            offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])]
+
+        for shape in offset_geo:
+            self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None)
+
+        if self.app.is_legacy is True:
+            self.shapes.redraw()
+
+    def delete_utility_geo(self):
+        self.shapes.clear(update=True)
+        self.shapes.redraw()
+
+    def on_mouse_move(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+        else:
+            event_pos = (event.xdata, event.ydata)
+
+        try:
+            x = float(event_pos[0])
+            y = float(event_pos[1])
+        except TypeError:
+            return
+
+        pos_canvas = self.app.plotcanvas.translate_coords((x, y))
+
+        # if GRID is active we need to get the snapped positions
+        if self.app.grid_status() == True:
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = pos_canvas
+
+        dx = pos[0] - self.origin[0]
+        dy = pos[1] - self.origin[1]
+
+        # delete the utility geometry
+        self.delete_utility_geo()
+        self.draw_utility_geo((dx, dy))
+
+    def on_mouse_release(self, event):
+        # mouse click will be accepted only if the left button is clicked
+        # this is necessary because right mouse click and middle mouse click
+        # are used for panning on the canvas
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+        else:
+            event_pos = (event.xdata, event.ydata)
+
+        if event.button == 1:
+            pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
+            self.delete_utility_geo()
+
+            # if GRID is active we need to get the snapped positions
+            if self.app.grid_status() == True:
+                pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+            else:
+                pos = pos_canvas
+
+            dx = pos[0] - self.origin[0]
+            dy = pos[1] - self.origin[1]
+
+            self.make(pos=(dx, dy))
+
+    def on_key_release(self, event):
+        pass
+
+    def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'):
+        """
+        Convert shapes from an SVG file into a geometry list.
+
+        :param filename: A String Stream file.
+        :param object_type: parameter passed further along. What kind the object will receive the SVG geometry
+        :param flip: Flip the vertically.
+        :type flip: bool
+        :param units: FlatCAM units
+        :return: None
+        """
+
+        # Parse into list of shapely objects
+        svg_tree = ET.parse(filename)
+        svg_root = svg_tree.getroot()
+
+        # Change origin to bottom left
+        # h = float(svg_root.get('height'))
+        # w = float(svg_root.get('width'))
+        h = svgparselength(svg_root.get('height'))[0]  # TODO: No units support yet
+        geos = getsvggeo(svg_root, object_type)
+
+        if flip:
+            geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
+
+        # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object
+        solid_geometry = list(self.flatten_list(geos))
+
+        geos_text = getsvgtext(svg_root, object_type, units=units)
+        if geos_text is not None:
+            geos_text_f = []
+            if flip:
+                # Change origin to bottom left
+                for i in geos_text:
+                    _, minimy, _, maximy = i.bounds
+                    h2 = (maximy - minimy) * 0.5
+                    geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
+            if geos_text_f:
+                solid_geometry += geos_text_f
+        return solid_geometry
+
+    def flatten_list(self, geo_list):
+        for item in geo_list:
+            if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
+                yield from self.flatten_list(item)
+            else:
+                yield item
+
+    def replot(self, obj):
+        def worker_task():
+            with self.app.proc_container.new('%s...' % _("Plotting")):
+                obj.plot()
+
+        self.app.worker_task.emit({'fcn': worker_task, 'params': []})
+
+    def on_exit(self):
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+            self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.mr)
+            self.app.plotcanvas.graph_event_disconnect(self.kr)
+
+        # delete the utility geometry
+        self.delete_utility_geo()
+        self.app.call_source = 'app'
+
+    def export_png_file(self):
+        text_data = self.text_data.get_value()
+        if text_data == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
+            return 'fail'
+
+        def job_thread_qr_png(app_obj, fname):
+            error_code = {
+                'L': qrcode.constants.ERROR_CORRECT_L,
+                'M': qrcode.constants.ERROR_CORRECT_M,
+                'Q': qrcode.constants.ERROR_CORRECT_Q,
+                'H': qrcode.constants.ERROR_CORRECT_H
+            }[self.error_radio.get_value()]
+
+            qr = qrcode.QRCode(
+                version=self.version_entry.get_value(),
+                error_correction=error_code,
+                box_size=self.bsize_entry.get_value(),
+                border=self.border_size_entry.get_value(),
+                image_factory=qrcode.image.pil.PilImage
+            )
+            qr.add_data(text_data)
+            qr.make(fit=True)
+
+            img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
+                                back_color=self.back_color_entry.get_value())
+            img.save(fname)
+
+            app_obj.call_source = 'qrcode_tool'
+
+        name = 'qr_code'
+
+        _filter = "PNG File (*.png);;All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export PNG"),
+                directory=self.app.get_last_save_folder() + '/' + str(name) + '_png',
+                filter=_filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export PNG"), filter=_filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL]%s' % _(" Export PNG cancelled."))
+            return
+        else:
+            self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
+
+    def export_svg_file(self):
+        text_data = self.text_data.get_value()
+        if text_data == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
+            return 'fail'
+
+        def job_thread_qr_svg(app_obj, fname):
+            error_code = {
+                'L': qrcode.constants.ERROR_CORRECT_L,
+                'M': qrcode.constants.ERROR_CORRECT_M,
+                'Q': qrcode.constants.ERROR_CORRECT_Q,
+                'H': qrcode.constants.ERROR_CORRECT_H
+            }[self.error_radio.get_value()]
+
+            qr = qrcode.QRCode(
+                version=self.version_entry.get_value(),
+                error_correction=error_code,
+                box_size=self.bsize_entry.get_value(),
+                border=self.border_size_entry.get_value(),
+                image_factory=qrcode.image.svg.SvgPathImage
+            )
+            qr.add_data(text_data)
+            img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
+                                back_color=self.back_color_entry.get_value())
+            img.save(fname)
+
+            app_obj.call_source = 'qrcode_tool'
+
+        name = 'qr_code'
+
+        _filter = "SVG File (*.svg);;All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export SVG"),
+                directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
+                filter=_filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG"), filter=_filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL]%s' % _(" Export SVG cancelled."))
+            return
+        else:
+            self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})
+
+    def on_qrcode_fill_color_entry(self):
+        color = self.fill_color_entry.get_value()
+        self.fill_color_button.setStyleSheet("background-color:%s" % str(color))
+
+    def on_qrcode_fill_color_button(self):
+        current_color = QtGui.QColor(self.fill_color_entry.get_value())
+
+        c_dialog = QtWidgets.QColorDialog()
+        fill_color = c_dialog.getColor(initial=current_color)
+
+        if fill_color.isValid() is False:
+            return
+
+        self.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name()))
+
+        new_val_sel = str(fill_color.name())
+        self.fill_color_entry.set_value(new_val_sel)
+
+    def on_qrcode_back_color_entry(self):
+        color = self.back_color_entry.get_value()
+        self.back_color_button.setStyleSheet("background-color:%s" % str(color))
+
+    def on_qrcode_back_color_button(self):
+        current_color = QtGui.QColor(self.back_color_entry.get_value())
+
+        c_dialog = QtWidgets.QColorDialog()
+        back_color = c_dialog.getColor(initial=current_color)
+
+        if back_color.isValid() is False:
+            return
+
+        self.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name()))
+
+        new_val_sel = str(back_color.name())
+        self.back_color_entry.set_value(new_val_sel)
+
+    def on_transparent_back_color(self, state):
+        if state:
+            self.back_color_entry.setDisabled(True)
+            self.back_color_button.setDisabled(True)
+            self.old_back_color = self.back_color_entry.get_value()
+            self.back_color_entry.set_value('transparent')
+        else:
+            self.back_color_entry.setDisabled(False)
+            self.back_color_button.setDisabled(False)
+            self.back_color_entry.set_value(self.old_back_color)

+ 24 - 4
flatcamTools/ToolRulesCheck.py

@@ -35,9 +35,9 @@ class RulesCheck(FlatCAMTool):
     tool_finished = QtCore.pyqtSignal(list)
     tool_finished = QtCore.pyqtSignal(list)
 
 
     def __init__(self, app):
     def __init__(self, app):
-        super(RulesCheck, self).__init__(self)
-        self.app = app
-        self.decimals = 4
+        self.decimals = app.decimals
+
+        FlatCAMTool.__init__(self, app)
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -493,10 +493,29 @@ class RulesCheck(FlatCAMTool):
               "In other words it creates multiple copies of the source object,\n"
               "In other words it creates multiple copies of the source object,\n"
               "arranged in a 2D array of rows and columns.")
               "arranged in a 2D array of rows and columns.")
         )
         )
+        self.run_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         hlay_2.addWidget(self.run_button)
         hlay_2.addWidget(self.run_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
         # #######################################################
         # #######################################################
         # ################ SIGNALS ##############################
         # ################ SIGNALS ##############################
         # #######################################################
         # #######################################################
@@ -520,6 +539,7 @@ class RulesCheck(FlatCAMTool):
         # self.app.collection.rowsInserted.connect(self.on_object_loaded)
         # self.app.collection.rowsInserted.connect(self.on_object_loaded)
 
 
         self.tool_finished.connect(self.on_tool_finished)
         self.tool_finished.connect(self.on_tool_finished)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
         # list to hold the temporary objects
         # list to hold the temporary objects
         self.objs = []
         self.objs = []
@@ -1160,7 +1180,7 @@ class RulesCheck(FlatCAMTool):
                     return
                     return
 
 
             # RULE: Check Copper to Outline Clearance
             # RULE: Check Copper to Outline Clearance
-            if self.clearance_copper2ol_cb.get_value():
+            if self.clearance_copper2ol_cb.get_value() and self.out_cb.get_value():
                 top_dict = dict()
                 top_dict = dict()
                 bottom_dict = dict()
                 bottom_dict = dict()
                 outline_dict = dict()
                 outline_dict = dict()

+ 128 - 38
flatcamTools/ToolSolderPaste.py

@@ -7,10 +7,11 @@
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMCommon import LoudDict
 from FlatCAMCommon import LoudDict
-from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCEntry2, FCTable, FCInputDialog
+from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCTable, FCInputDialog, FCDoubleSpinner, FCSpinner
 from FlatCAMApp import log
 from FlatCAMApp import log
 from camlib import distance
 from camlib import distance
 from FlatCAMObj import FlatCAMCNCjob
 from FlatCAMObj import FlatCAMCNCjob
+from flatcamEditors.FlatCAMTextEditor import TextEditor
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt
 from PyQt5.QtCore import Qt
@@ -38,6 +39,9 @@ class SolderPaste(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
+        # Number of decimals to be used for tools/nozzles in this FlatCAM Tool
+        self.decimals = self.app.decimals
+
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
@@ -101,7 +105,10 @@ class SolderPaste(FlatCAMTool):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new Nozzle tool to add in the Tool Table")
             _("Diameter for the new Nozzle tool to add in the Tool Table")
         )
         )
-        self.addtool_entry = FCEntry2()
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_range(0.0000001, 9999.9999)
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.setSingleStep(0.1)
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -127,6 +134,12 @@ class SolderPaste(FlatCAMTool):
         self.soldergeo_btn.setToolTip(
         self.soldergeo_btn.setToolTip(
             _("Generate solder paste dispensing geometry.")
             _("Generate solder paste dispensing geometry.")
         )
         )
+        self.soldergeo_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
 
         grid0.addWidget(self.addtool_btn, 0, 0)
         grid0.addWidget(self.addtool_btn, 0, 0)
         # grid2.addWidget(self.copytool_btn, 0, 1)
         # grid2.addWidget(self.copytool_btn, 0, 1)
@@ -161,7 +174,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_box.addLayout(self.gcode_form_layout)
         self.gcode_box.addLayout(self.gcode_form_layout)
 
 
         # Z dispense start
         # Z dispense start
-        self.z_start_entry = FCEntry()
+        self.z_start_entry = FCDoubleSpinner()
+        self.z_start_entry.set_range(0.0000001, 9999.9999)
+        self.z_start_entry.set_precision(self.decimals)
+        self.z_start_entry.setSingleStep(0.1)
+
         self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start"))
         self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start"))
         self.z_start_label.setToolTip(
         self.z_start_label.setToolTip(
             _("The height (Z) when solder paste dispensing starts.")
             _("The height (Z) when solder paste dispensing starts.")
@@ -169,7 +186,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry)
         self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry)
 
 
         # Z dispense
         # Z dispense
-        self.z_dispense_entry = FCEntry()
+        self.z_dispense_entry = FCDoubleSpinner()
+        self.z_dispense_entry.set_range(0.0000001, 9999.9999)
+        self.z_dispense_entry.set_precision(self.decimals)
+        self.z_dispense_entry.setSingleStep(0.1)
+
         self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense"))
         self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense"))
         self.z_dispense_label.setToolTip(
         self.z_dispense_label.setToolTip(
             _("The height (Z) when doing solder paste dispensing.")
             _("The height (Z) when doing solder paste dispensing.")
@@ -177,7 +198,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry)
         self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry)
 
 
         # Z dispense stop
         # Z dispense stop
-        self.z_stop_entry = FCEntry()
+        self.z_stop_entry = FCDoubleSpinner()
+        self.z_stop_entry.set_range(0.0000001, 9999.9999)
+        self.z_stop_entry.set_precision(self.decimals)
+        self.z_stop_entry.setSingleStep(0.1)
+
         self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop"))
         self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop"))
         self.z_stop_label.setToolTip(
         self.z_stop_label.setToolTip(
             _("The height (Z) when solder paste dispensing stops.")
             _("The height (Z) when solder paste dispensing stops.")
@@ -185,7 +210,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry)
         self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry)
 
 
         # Z travel
         # Z travel
-        self.z_travel_entry = FCEntry()
+        self.z_travel_entry = FCDoubleSpinner()
+        self.z_travel_entry.set_range(0.0000001, 9999.9999)
+        self.z_travel_entry.set_precision(self.decimals)
+        self.z_travel_entry.setSingleStep(0.1)
+
         self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel"))
         self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel"))
         self.z_travel_label.setToolTip(
         self.z_travel_label.setToolTip(
            _("The height (Z) for travel between pads\n"
            _("The height (Z) for travel between pads\n"
@@ -194,7 +223,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry)
         self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry)
 
 
         # Z toolchange location
         # Z toolchange location
-        self.z_toolchange_entry = FCEntry()
+        self.z_toolchange_entry = FCDoubleSpinner()
+        self.z_toolchange_entry.set_range(0.0000001, 9999.9999)
+        self.z_toolchange_entry.set_precision(self.decimals)
+        self.z_toolchange_entry.setSingleStep(0.1)
+
         self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange"))
         self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange"))
         self.z_toolchange_label.setToolTip(
         self.z_toolchange_label.setToolTip(
            _("The height (Z) for tool (nozzle) change.")
            _("The height (Z) for tool (nozzle) change.")
@@ -211,7 +244,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry)
         self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry)
 
 
         # Feedrate X-Y
         # Feedrate X-Y
-        self.frxy_entry = FCEntry()
+        self.frxy_entry = FCDoubleSpinner()
+        self.frxy_entry.set_range(0.0000001, 9999.9999)
+        self.frxy_entry.set_precision(self.decimals)
+        self.frxy_entry.setSingleStep(0.1)
+
         self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y"))
         self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y"))
         self.frxy_label.setToolTip(
         self.frxy_label.setToolTip(
            _("Feedrate (speed) while moving on the X-Y plane.")
            _("Feedrate (speed) while moving on the X-Y plane.")
@@ -219,7 +256,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry)
         self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry)
 
 
         # Feedrate Z
         # Feedrate Z
-        self.frz_entry = FCEntry()
+        self.frz_entry = FCDoubleSpinner()
+        self.frz_entry.set_range(0.0000001, 9999.9999)
+        self.frz_entry.set_precision(self.decimals)
+        self.frz_entry.setSingleStep(0.1)
+
         self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
         self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
         self.frz_label.setToolTip(
         self.frz_label.setToolTip(
             _("Feedrate (speed) while moving vertically\n"
             _("Feedrate (speed) while moving vertically\n"
@@ -228,7 +269,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.frz_label, self.frz_entry)
         self.gcode_form_layout.addRow(self.frz_label, self.frz_entry)
 
 
         # Feedrate Z Dispense
         # Feedrate Z Dispense
-        self.frz_dispense_entry = FCEntry()
+        self.frz_dispense_entry = FCDoubleSpinner()
+        self.frz_dispense_entry.set_range(0.0000001, 9999.9999)
+        self.frz_dispense_entry.set_precision(self.decimals)
+        self.frz_dispense_entry.setSingleStep(0.1)
+
         self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense"))
         self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense"))
         self.frz_dispense_label.setToolTip(
         self.frz_dispense_label.setToolTip(
            _("Feedrate (speed) while moving up vertically\n"
            _("Feedrate (speed) while moving up vertically\n"
@@ -237,7 +282,10 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry)
         self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry)
 
 
         # Spindle Speed Forward
         # Spindle Speed Forward
-        self.speedfwd_entry = FCEntry()
+        self.speedfwd_entry = FCSpinner()
+        self.speedfwd_entry.set_range(0, 999999)
+        self.speedfwd_entry.setSingleStep(1000)
+
         self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD"))
         self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD"))
         self.speedfwd_label.setToolTip(
         self.speedfwd_label.setToolTip(
            _("The dispenser speed while pushing solder paste\n"
            _("The dispenser speed while pushing solder paste\n"
@@ -246,7 +294,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry)
         self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry)
 
 
         # Dwell Forward
         # Dwell Forward
-        self.dwellfwd_entry = FCEntry()
+        self.dwellfwd_entry = FCDoubleSpinner()
+        self.dwellfwd_entry.set_range(0.0000001, 9999.9999)
+        self.dwellfwd_entry.set_precision(self.decimals)
+        self.dwellfwd_entry.setSingleStep(0.1)
+
         self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
         self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
         self.dwellfwd_label.setToolTip(
         self.dwellfwd_label.setToolTip(
             _("Pause after solder dispensing.")
             _("Pause after solder dispensing.")
@@ -254,7 +306,10 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry)
         self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry)
 
 
         # Spindle Speed Reverse
         # Spindle Speed Reverse
-        self.speedrev_entry = FCEntry()
+        self.speedrev_entry = FCSpinner()
+        self.speedrev_entry.set_range(0, 999999)
+        self.speedrev_entry.setSingleStep(1000)
+
         self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV"))
         self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV"))
         self.speedrev_label.setToolTip(
         self.speedrev_label.setToolTip(
            _("The dispenser speed while retracting solder paste\n"
            _("The dispenser speed while retracting solder paste\n"
@@ -263,7 +318,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry)
         self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry)
 
 
         # Dwell Reverse
         # Dwell Reverse
-        self.dwellrev_entry = FCEntry()
+        self.dwellrev_entry = FCDoubleSpinner()
+        self.dwellrev_entry.set_range(0.0000001, 9999.9999)
+        self.dwellrev_entry.set_precision(self.decimals)
+        self.dwellrev_entry.setSingleStep(0.1)
+
         self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
         self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
         self.dwellrev_label.setToolTip(
         self.dwellrev_label.setToolTip(
             _("Pause after solder paste dispenser retracted,\n"
             _("Pause after solder paste dispenser retracted,\n"
@@ -271,7 +330,7 @@ class SolderPaste(FlatCAMTool):
         )
         )
         self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
         self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
 
 
-        # Postprocessors
+        # Preprocessors
         pp_label = QtWidgets.QLabel('%s:' % _('PostProcessor'))
         pp_label = QtWidgets.QLabel('%s:' % _('PostProcessor'))
         pp_label.setToolTip(
         pp_label.setToolTip(
             _("Files that control the GCode generation.")
             _("Files that control the GCode generation.")
@@ -290,6 +349,12 @@ class SolderPaste(FlatCAMTool):
            _("Generate GCode for Solder Paste dispensing\n"
            _("Generate GCode for Solder Paste dispensing\n"
              "on PCB pads.")
              "on PCB pads.")
         )
         )
+        self.solder_gcode_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
 
         self.generation_frame = QtWidgets.QFrame()
         self.generation_frame = QtWidgets.QFrame()
         self.generation_frame.setContentsMargins(0, 0, 0, 0)
         self.generation_frame.setContentsMargins(0, 0, 0, 0)
@@ -370,12 +435,24 @@ class SolderPaste(FlatCAMTool):
             _("View the generated GCode for Solder Paste dispensing\n"
             _("View the generated GCode for Solder Paste dispensing\n"
               "on PCB pads.")
               "on PCB pads.")
         )
         )
+        self.solder_gcode_view_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
 
         self.solder_gcode_save_btn = QtWidgets.QPushButton(_("Save GCode"))
         self.solder_gcode_save_btn = QtWidgets.QPushButton(_("Save GCode"))
         self.solder_gcode_save_btn.setToolTip(
         self.solder_gcode_save_btn.setToolTip(
            _("Save the generated GCode for Solder Paste dispensing\n"
            _("Save the generated GCode for Solder Paste dispensing\n"
              "on PCB pads, to a file.")
              "on PCB pads, to a file.")
         )
         )
+        self.solder_gcode_save_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
 
         step4_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 4'))
         step4_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 4'))
         step4_lbl.setToolTip(
         step4_lbl.setToolTip(
@@ -385,10 +462,23 @@ class SolderPaste(FlatCAMTool):
 
 
         grid4.addWidget(step4_lbl, 0, 0)
         grid4.addWidget(step4_lbl, 0, 0)
         grid4.addWidget(self.solder_gcode_view_btn, 0, 2)
         grid4.addWidget(self.solder_gcode_view_btn, 0, 2)
-        grid4.addWidget(self.solder_gcode_save_btn, 1, 2)
+        grid4.addWidget(self.solder_gcode_save_btn, 1, 0, 1, 3)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
         # self.gcode_frame.setDisabled(True)
         # self.gcode_frame.setDisabled(True)
         # self.save_gcode_frame.setDisabled(True)
         # self.save_gcode_frame.setDisabled(True)
 
 
@@ -401,8 +491,7 @@ class SolderPaste(FlatCAMTool):
         self.units = ''
         self.units = ''
         self.name = ""
         self.name = ""
 
 
-        # Number of decimals to be used for tools/nozzles in this FlatCAM Tool
-        self.decimals = 4
+        self.text_editor_tab = None
 
 
         # this will be used in the combobox context menu, for delete entry
         # this will be used in the combobox context menu, for delete entry
         self.obj_to_be_deleted_name = ''
         self.obj_to_be_deleted_name = ''
@@ -416,7 +505,7 @@ class SolderPaste(FlatCAMTool):
         # ## Signals
         # ## Signals
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
-        self.addtool_entry.editingFinished.connect(self.on_tool_add)
+        self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)
@@ -427,6 +516,7 @@ class SolderPaste(FlatCAMTool):
         self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select)
         self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select)
 
 
         self.app.object_status_changed.connect(self.update_comboboxes)
         self.app.object_status_changed.connect(self.update_comboboxes)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def run(self, toggle=True):
     def run(self, toggle=True):
         self.app.report_usage("ToolSolderPaste()")
         self.app.report_usage("ToolSolderPaste()")
@@ -505,7 +595,7 @@ class SolderPaste(FlatCAMTool):
 
 
         try:
         try:
             dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",") if dia != '']
             dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",") if dia != '']
-        except:
+        except Exception:
             log.error("At least one Nozzle tool diameter needed. "
             log.error("At least one Nozzle tool diameter needed. "
                       "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.")
                       "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.")
             return
             return
@@ -526,15 +616,15 @@ class SolderPaste(FlatCAMTool):
         self.name = ""
         self.name = ""
         self.obj = None
         self.obj = None
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
             self.decimals = 4
             self.decimals = 4
         else:
         else:
             self.decimals = 2
             self.decimals = 2
 
 
-        for name in list(self.app.postprocessors.keys()):
-            # populate only with postprocessor files that start with 'Paste_'
+        for name in list(self.app.preprocessors.keys()):
+            # populate only with preprocessor files that start with 'Paste_'
             if name.partition('_')[0] != 'Paste':
             if name.partition('_')[0] != 'Paste':
                 continue
                 continue
             self.pp_combo.addItem(name)
             self.pp_combo.addItem(name)
@@ -549,7 +639,7 @@ class SolderPaste(FlatCAMTool):
         self.ui_disconnect()
         self.ui_disconnect()
 
 
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.tooltable_tools.items():
         for k, v in self.tooltable_tools.items():
@@ -623,7 +713,7 @@ class SolderPaste(FlatCAMTool):
         if row is None:
         if row is None:
             try:
             try:
                 current_row = self.tools_table.currentRow()
                 current_row = self.tools_table.currentRow()
-            except:
+            except Exception:
                 current_row = 0
                 current_row = 0
         else:
         else:
             current_row = row
             current_row = row
@@ -1235,9 +1325,9 @@ class SolderPaste(FlatCAMTool):
             xmax = obj.options['xmax']
             xmax = obj.options['xmax']
             ymax = obj.options['ymax']
             ymax = obj.options['ymax']
         except Exception as e:
         except Exception as e:
-            log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e))
+            log.debug("SolderPaste.on_create_gcode() --> %s\n" % str(e))
             msg = '[ERROR] %s' % _("An internal error has ocurred. See shell.\n")
             msg = '[ERROR] %s' % _("An internal error has ocurred. See shell.\n")
-            msg += 'FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s' % str(e)
+            msg += 'SolderPaste.on_create_gcode() --> %s' % str(e)
             msg += traceback.format_exc()
             msg += traceback.format_exc()
             self.app.inform.emit(msg)
             self.app.inform.emit(msg)
             return
             return
@@ -1325,14 +1415,14 @@ class SolderPaste(FlatCAMTool):
         """
         """
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
 
 
-        # add the tab if it was closed
-        self.app.ui.plot_tab_area.addTab(self.app.ui.text_editor_tab, _("Code Editor"))
+        self.text_editor_tab = TextEditor(app=self.app)
 
 
-        # first clear previous text in text editor (if any)
-        self.app.ui.code_editor.clear()
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.text_editor_tab, _("SP GCode Editor"))
+        self.text_editor_tab.setObjectName('solderpaste_gcode_editor_tab')
 
 
         # Switch plot_area to CNCJob tab
         # Switch plot_area to CNCJob tab
-        self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.text_editor_tab)
+        self.app.ui.plot_tab_area.setCurrentWidget(self.text_editor_tab)
 
 
         name = self.cnc_obj_combo.currentText()
         name = self.cnc_obj_combo.currentText()
         obj = self.app.collection.get_by_name(name)
         obj = self.app.collection.get_by_name(name)
@@ -1376,21 +1466,21 @@ class SolderPaste(FlatCAMTool):
         try:
         try:
             for line in lines:
             for line in lines:
                 proc_line = str(line).strip('\n')
                 proc_line = str(line).strip('\n')
-                self.app.ui.code_editor.append(proc_line)
+                self.text_editor_tab.code_editor.append(proc_line)
         except Exception as e:
         except Exception as e:
             log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e))
             log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e))
             self.app.inform.emit('[ERROR] %s --> %s' %
             self.app.inform.emit('[ERROR] %s --> %s' %
                                  ('ToolSolderPaste.on_view_gcode()', str(e)))
                                  ('ToolSolderPaste.on_view_gcode()', str(e)))
             return
             return
 
 
-        self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        self.text_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
 
 
-        self.app.handleTextChanged()
-        self.app.ui.show()
+        self.text_editor_tab.handleTextChanged()
+        # self.app.ui.show()
 
 
     def on_save_gcode(self):
     def on_save_gcode(self):
         """
         """
-        Save sodlerpaste dispensing GCode to a file on HDD.
+        Save solderpaste dispensing GCode to a file on HDD.
 
 
         :return:
         :return:
         """
         """
@@ -1405,7 +1495,7 @@ class SolderPaste(FlatCAMTool):
             return
             return
 
 
         _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
         _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
-                   "G-Code Files (*.g-code);;All Files (*.*)"
+                   "G-Code Files (*.g-code);;All Files (*.*);;G-Code Files (*.gcode);;G-Code Files (*.ngc)"
 
 
         try:
         try:
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)

+ 46 - 17
flatcamTools/ToolSub.py

@@ -36,6 +36,7 @@ class ToolSub(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
+        self.decimals = self.app.decimals
 
 
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
@@ -100,6 +101,12 @@ class ToolSub(FlatCAMTool):
               "Can be used to remove the overlapping silkscreen\n"
               "Can be used to remove the overlapping silkscreen\n"
               "over the soldermask.")
               "over the soldermask.")
         )
         )
+        self.intersect_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.intersect_btn)
         self.tools_box.addWidget(self.intersect_btn)
         self.tools_box.addWidget(e_lab_1)
         self.tools_box.addWidget(e_lab_1)
 
 
@@ -148,11 +155,30 @@ class ToolSub(FlatCAMTool):
             _("Will remove the area occupied by the subtractor\n"
             _("Will remove the area occupied by the subtractor\n"
               "Geometry from the Target Geometry.")
               "Geometry from the Target Geometry.")
         )
         )
+        self.intersect_geo_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.intersect_geo_btn)
         self.tools_box.addWidget(self.intersect_geo_btn)
         self.tools_box.addWidget(e_lab_1)
         self.tools_box.addWidget(e_lab_1)
 
 
         self.tools_box.addStretch()
         self.tools_box.addStretch()
 
 
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.tools_box.addWidget(self.reset_button)
+
         # QTimer for periodic check
         # QTimer for periodic check
         self.check_thread = QtCore.QTimer()
         self.check_thread = QtCore.QTimer()
         # Every time an intersection job is started we add a promise; every time an intersection job is finished
         # Every time an intersection job is started we add a promise; every time an intersection job is finished
@@ -198,6 +224,7 @@ class ToolSub(FlatCAMTool):
             pass
             pass
         self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
         self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
         self.job_finished.connect(self.on_job_finished)
         self.job_finished.connect(self.on_job_finished)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
@@ -248,23 +275,22 @@ class ToolSub(FlatCAMTool):
 
 
         self.target_grb_obj_name = self.target_gerber_combo.currentText()
         self.target_grb_obj_name = self.target_gerber_combo.currentText()
         if self.target_grb_obj_name == '':
         if self.target_grb_obj_name == '':
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No Target object loaded."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
             return
             return
 
 
+        self.app.inform.emit('%s' % _("Loading geometry from Gerber objects."))
+
         # Get target object.
         # Get target object.
         try:
         try:
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
         except Exception as e:
         except Exception as e:
             log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
             log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                 (_("Could not retrieve object"), self.obj_name))
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
             return "Could not retrieve object: %s" % self.target_grb_obj_name
             return "Could not retrieve object: %s" % self.target_grb_obj_name
 
 
         self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
         self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
         if self.sub_grb_obj_name == '':
         if self.sub_grb_obj_name == '':
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No Subtractor object loaded."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
             return
             return
 
 
         # Get substractor object.
         # Get substractor object.
@@ -272,20 +298,19 @@ class ToolSub(FlatCAMTool):
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
         except Exception as e:
         except Exception as e:
             log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
             log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                 (_("Could not retrieve object"), self.obj_name))
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
             return "Could not retrieve object: %s" % self.sub_grb_obj_name
             return "Could not retrieve object: %s" % self.sub_grb_obj_name
 
 
         # crate the new_apertures dict structure
         # crate the new_apertures dict structure
         for apid in self.target_grb_obj.apertures:
         for apid in self.target_grb_obj.apertures:
-            self.new_apertures[apid] = {}
+            self.new_apertures[apid] = dict()
             self.new_apertures[apid]['type'] = 'C'
             self.new_apertures[apid]['type'] = 'C'
             self.new_apertures[apid]['size'] = self.target_grb_obj.apertures[apid]['size']
             self.new_apertures[apid]['size'] = self.target_grb_obj.apertures[apid]['size']
-            self.new_apertures[apid]['geometry'] = []
+            self.new_apertures[apid]['geometry'] = list()
 
 
-        geo_solid_union_list = []
-        geo_follow_union_list = []
-        geo_clear_union_list = []
+        geo_solid_union_list = list()
+        geo_follow_union_list = list()
+        geo_clear_union_list = list()
 
 
         for apid1 in self.sub_grb_obj.apertures:
         for apid1 in self.sub_grb_obj.apertures:
             if 'geometry' in self.sub_grb_obj.apertures[apid1]:
             if 'geometry' in self.sub_grb_obj.apertures[apid1]:
@@ -297,6 +322,7 @@ class ToolSub(FlatCAMTool):
                     if 'clear' in elem:
                     if 'clear' in elem:
                         geo_clear_union_list.append(elem['clear'])
                         geo_clear_union_list.append(elem['clear'])
 
 
+        self.app.inform.emit('%s' % _("Processing geometry from Subtractor Gerber object."))
         self.sub_solid_union = cascaded_union(geo_solid_union_list)
         self.sub_solid_union = cascaded_union(geo_solid_union_list)
         self.sub_follow_union = cascaded_union(geo_follow_union_list)
         self.sub_follow_union = cascaded_union(geo_follow_union_list)
         self.sub_clear_union = cascaded_union(geo_clear_union_list)
         self.sub_clear_union = cascaded_union(geo_clear_union_list)
@@ -310,15 +336,15 @@ class ToolSub(FlatCAMTool):
 
 
         for apid in self.target_grb_obj.apertures:
         for apid in self.target_grb_obj.apertures:
             geo = self.target_grb_obj.apertures[apid]['geometry']
             geo = self.target_grb_obj.apertures[apid]['geometry']
-            self.app.worker_task.emit({'fcn': self.aperture_intersection,
-                                       'params': [apid, geo]})
+            self.app.worker_task.emit({'fcn': self.aperture_intersection, 'params': [apid, geo]})
 
 
     def aperture_intersection(self, apid, geo):
     def aperture_intersection(self, apid, geo):
-        new_geometry = []
+        new_geometry = list()
 
 
         log.debug("Working on promise: %s" % str(apid))
         log.debug("Working on promise: %s" % str(apid))
 
 
-        with self.app.proc_container.new('%s: %s...' % (_("Parsing geometry for aperture", str(apid)))):
+        with self.app.proc_container.new('%s: %s...' % (_("Parsing geometry for aperture"), str(apid))):
+
             for geo_el in geo:
             for geo_el in geo:
                 new_el = dict()
                 new_el = dict()
 
 
@@ -378,6 +404,8 @@ class ToolSub(FlatCAMTool):
 
 
                 new_geometry.append(deepcopy(new_el))
                 new_geometry.append(deepcopy(new_el))
 
 
+        self.app.inform.emit('%s: %s...' % (_("Finished parsing geometry for aperture"), str(apid)))
+
         if new_geometry:
         if new_geometry:
             while not self.new_apertures[apid]['geometry']:
             while not self.new_apertures[apid]['geometry']:
                 self.new_apertures[apid]['geometry'] = deepcopy(new_geometry)
                 self.new_apertures[apid]['geometry'] = deepcopy(new_geometry)
@@ -412,6 +440,7 @@ class ToolSub(FlatCAMTool):
                 poly_buff = work_poly_buff.buffer(0.0000001)
                 poly_buff = work_poly_buff.buffer(0.0000001)
             except ValueError:
             except ValueError:
                 pass
                 pass
+
             try:
             try:
                 poly_buff = poly_buff.buffer(-0.0000001)
                 poly_buff = poly_buff.buffer(-0.0000001)
             except ValueError:
             except ValueError:

+ 2 - 2
flatcamTools/ToolTransform.py

@@ -30,7 +30,7 @@ class ToolTransform(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
         self.layout.addLayout(self.transform_lay)
@@ -371,7 +371,7 @@ class ToolTransform(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+E', **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs)
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
         # ## Initialize form
         # ## Initialize form

+ 5 - 0
flatcamTools/__init__.py

@@ -2,6 +2,7 @@ import sys
 
 
 
 
 from flatcamTools.ToolCalculators import ToolCalculator
 from flatcamTools.ToolCalculators import ToolCalculator
+from flatcamTools.ToolCalibration import ToolCalibration
 from flatcamTools.ToolCutOut import CutOut
 from flatcamTools.ToolCutOut import CutOut
 
 
 from flatcamTools.ToolDblSided import DblSidedTool
 from flatcamTools.ToolDblSided import DblSidedTool
@@ -25,8 +26,12 @@ from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
 from flatcamTools.ToolPDF import ToolPDF
 from flatcamTools.ToolProperties import Properties
 from flatcamTools.ToolProperties import Properties
 
 
+from flatcamTools.ToolQRCode import QRCode
 from flatcamTools.ToolRulesCheck import RulesCheck
 from flatcamTools.ToolRulesCheck import RulesCheck
 
 
+from flatcamTools.ToolCopperThieving import ToolCopperThieving
+from flatcamTools.ToolFiducials import ToolFiducials
+
 from flatcamTools.ToolShell import FCShell
 from flatcamTools.ToolShell import FCShell
 from flatcamTools.ToolSolderPaste import SolderPaste
 from flatcamTools.ToolSolderPaste import SolderPaste
 from flatcamTools.ToolSub import ToolSub
 from flatcamTools.ToolSub import ToolSub

BIN
locale/de/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 343 - 270
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 321 - 279
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 377 - 265
locale/es/LC_MESSAGES/strings.po


BIN
locale/fr/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 376 - 264
locale/fr/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 375 - 264
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 335 - 270
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


Plik diff jest za duży
+ 407 - 270
locale/ru/LC_MESSAGES/strings.po


Plik diff jest za duży
+ 428 - 354
locale_template/strings.pot


+ 24 - 9
make_win.py → make_freezed.py

@@ -25,7 +25,10 @@
 #   scipy.sparse.sparsetools._csr.pyd, scipy.sparse.sparsetools._csc.pyd,
 #   scipy.sparse.sparsetools._csr.pyd, scipy.sparse.sparsetools._csc.pyd,
 #   scipy.sparse.sparsetools._coo.pyd
 #   scipy.sparse.sparsetools._coo.pyd
 
 
-import os, site, sys, platform
+import os
+import site
+import sys
+import platform
 from cx_Freeze import setup, Executable
 from cx_Freeze import setup, Executable
 
 
 # this is done to solve the tkinter not being found
 # this is done to solve the tkinter not being found
@@ -54,7 +57,7 @@ if platform.architecture()[0] == '64bit':
     include_files.append((os.path.join(site_dir, "ortools"), "ortools"))
     include_files.append((os.path.join(site_dir, "ortools"), "ortools"))
 
 
 include_files.append(("locale", "lib/locale"))
 include_files.append(("locale", "lib/locale"))
-include_files.append(("postprocessors", "lib/postprocessors"))
+include_files.append(("preprocessors", "lib/preprocessors"))
 include_files.append(("share", "lib/share"))
 include_files.append(("share", "lib/share"))
 include_files.append(("flatcamGUI/VisPyData", "lib/vispy"))
 include_files.append(("flatcamGUI/VisPyData", "lib/vispy"))
 include_files.append(("config", "lib/config"))
 include_files.append(("config", "lib/config"))
@@ -71,11 +74,10 @@ if sys.platform == "win32":
 if platform.architecture()[0] == '64bit':
 if platform.architecture()[0] == '64bit':
     buildOptions = dict(
     buildOptions = dict(
         include_files=include_files,
         include_files=include_files,
-        excludes=['scipy','pytz'],
+        excludes=['scipy', 'pytz'],
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # packages=['numpy','google', 'rasterio'] # works for Python 3.7
         # packages=['numpy','google', 'rasterio'] # works for Python 3.7
-        packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1
-
+        packages=['opengl', 'numpy', 'google', 'rasterio'],   # works for Python 3.6.5 and Python 3.7.1
     )
     )
 else:
 else:
     buildOptions = dict(
     buildOptions = dict(
@@ -83,13 +85,26 @@ else:
         excludes=['scipy', 'pytz'],
         excludes=['scipy', 'pytz'],
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # packages=['numpy', 'rasterio']  # works for Python 3.7
         # packages=['numpy', 'rasterio']  # works for Python 3.7
-        packages = ['opengl', 'numpy', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1
-
+        packages=['opengl', 'numpy', 'rasterio'],   # works for Python 3.6.5 and Python 3.7.1
     )
     )
 
 
+if sys.platform == "win32":
+    buildOptions["include_msvcr"] = True
+
 print("INCLUDE_FILES", include_files)
 print("INCLUDE_FILES", include_files)
 
 
-# execfile('clean.py')
+
+def getTargetName():
+    my_OS = platform.system()
+    if my_OS == 'Linux':
+        return "FlatCAM"
+    elif my_OS == 'Windows':
+        return "FlatCAM.exe"
+    else:
+        return "FlatCAM.dmg"
+
+
+exe = Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base, targetName=getTargetName())
 
 
 setup(
 setup(
     name="FlatCAM",
     name="FlatCAM",
@@ -97,5 +112,5 @@ setup(
     version="8.9",
     version="8.9",
     description="FlatCAM: 2D Computer Aided PCB Manufacturing",
     description="FlatCAM: 2D Computer Aided PCB Manufacturing",
     options=dict(build_exe=buildOptions),
     options=dict(build_exe=buildOptions),
-    executables=[Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base)]
+    executables=[exe]
 )
 )

+ 225 - 0
preprocessors/Berta_CNC.py

@@ -0,0 +1,225 @@
+##############################################################
+# FlatCAM: 2D Post-processing for Manufacturing              #
+# http://flatcam.org                                         #
+# File Author: Matthieu Berthomé                             #
+# Date: 5/26/2017                                            #
+#                                                            #
+# Correction & Adaptation for Berta CNC machine              #
+# Date: 24/10/2019                                           #
+#                                                            #
+# MIT Licence                                                #
+##############################################################
+
+from FlatCAMPostProc import *
+
+
+class Berta_CNC(FlatCAMPostProc):
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['xy_toolchange']
+        gcode = ''
+
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['z_feedrate']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['z_depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['z_depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
+
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
+
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['z_end']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
+        else:
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += '(Berta)\n'
+        gcode += 'G90 G94 G17 G91.1'
+        gcode += (
+            # This line allow you to sets the machine to METRIC / INCH in the GUI
+            'G20\n' if p.units.upper() == 'IN' else 'G21\n')
+        #        gcode += 'G21\n' # This line sets the machine to METRIC ONLY
+        #        gcode += 'G20\n' # This line sets the machine to INCH ONLY
+        gcode += 'G64 P0.03\n'
+        gcode += 'M110\n'
+        gcode += 'G54\n'
+        gcode += 'G0\n'
+        gcode += '(Berta)\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format % (p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        z_toolchange = p.z_toolchange
+        toolchangexy = p.xy_toolchange
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            x_toolchange = toolchangexy[0]
+            y_toolchange = toolchangexy[1]
+        else:
+            x_toolchange = 0
+            y_toolchange = 0
+
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            z_toolchange = p.startz
+
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
+
+        if str(p['options']['type']) == 'Excellon':
+            for i in p['options']['Tools_in_use']:
+                if i[0] == p.tool:
+                    no_drills = i[2]
+
+            if toolchangexy is not None:
+                gcode = """
+M5
+G00 Z{z_toolchange}
+G00 X{x_toolchange} Y{y_toolchange}                
+T{tool}
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+            else:
+                gcode = """
+M5       
+G00 Z{z_toolchange}
+T{tool}
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+        else:
+            if toolchangexy is not None:
+                gcode = """
+M5
+G00 Z{z_toolchange}
+G00 X{x_toolchange} Y{y_toolchange}
+T{tool}
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+            else:
+                gcode = """
+M5
+G00 Z{z_toolchange}
+T{tool}
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['xy_toolchange']
+        gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.z_end) + "\n")
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+
+        gcode += '(Berta)\n'
+        gcode += 'M111\n'
+        gcode += 'M30\n'
+        gcode += '(Berta)\n'
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.feedrate))
+
+    def z_feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.z_feedrate))
+
+    def spindle_code(self, p):
+        sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir]
+        if p.spindlespeed:
+            return '%s S%s' % (sdir, str(p.spindlespeed))
+        else:
+            return sdir
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self, p):
+        return 'M05'

+ 157 - 0
preprocessors/ISEL_CNC.py

@@ -0,0 +1,157 @@
+# ########################################################## ##
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Matthieu Berthomé                           #
+# Date: 5/26/2017                                          #
+# MIT Licence                                              #
+# ########################################################## ##
+
+from FlatCAMPostProc import *
+
+
+class ISEL_CNC(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['xy_toolchange']
+        gcode = ''
+
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['z_feedrate']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['z_depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['z_depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
+
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
+
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['z_end']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
+        else:
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += 'G71\n'
+        gcode += 'G90\n'
+        gcode += 'G94\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        f_plunge = p.f_plunge
+        no_drills = 1
+
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
+
+        if str(p['options']['type']) == 'Excellon':
+            for i in p['options']['Tools_in_use']:
+                if i[0] == p.tool:
+                    no_drills = i[2]
+
+            gcode = """
+M05       
+T{tool}
+M06
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M01""".format(tool=int(p.tool), t_drills=no_drills, toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+        else:
+            gcode = """
+M05
+T{tool}
+M06    
+(MSG, Change to Tool Dia = {toolC})
+M01""".format(tool=int(p.tool), toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['xy_toolchange']
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.z_end) + "\n")
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def z_feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.z_feedrate))
+
+    def spindle_code(self, p):
+        sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir]
+        if p.spindlespeed:
+            return '%s S%s' % (sdir, str(p.spindlespeed))
+        else:
+            return sdir
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 9 - 6
postprocessors/Paste_1.py → preprocessors/Paste_1.py

@@ -36,10 +36,11 @@ class Paste_1(FlatCAMPostProc_Tools):
         gcode += '(Z_Travel: ' + str(p['z_travel']) + units + ')\n'
         gcode += '(Z_Travel: ' + str(p['z_travel']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
 
 
-        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                       p.decimals, coords_xy[1]) + units + ')\n'
 
 
         if 'Paste' in p.pp_solderpaste_name:
         if 'Paste' in p.pp_solderpaste_name:
-            gcode += '(Postprocessor SolderPaste Dispensing Geometry: ' + str(p.pp_solderpaste_name) + ')\n' + '\n'
+            gcode += '(Preprocessor SolderPaste Dispensing Geometry: ' + str(p.pp_solderpaste_name) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -74,11 +75,11 @@ class Paste_1(FlatCAMPostProc_Tools):
         if toolchangexy is not None:
         if toolchangexy is not None:
             x_toolchange = toolchangexy[0]
             x_toolchange = toolchangexy[0]
             y_toolchange = toolchangexy[1]
             y_toolchange = toolchangexy[1]
-
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(float(p['toolC']), '.2f')
         else:
         else:
-            toolC_formatted = format(float(p['toolC']), '.4f')
+            x_toolchange = 0.0
+            y_toolchange = 0.0
+
+        toolC_formatted = '%.*f' % (p.decimals, float(p['toolC']))
 
 
         if toolchangexy is not None:
         if toolchangexy is not None:
             gcode = """
             gcode = """
@@ -88,6 +89,7 @@ T{tool}
 M6    
 M6    
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 M0
 M0
+G00 Z{z_toolchange}
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -101,6 +103,7 @@ T{tool}
 M6    
 M6    
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 M0
 M0
+G00 Z{z_toolchange}
 """.format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
 """.format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            tool=int(int(p.tool)),
            tool=int(int(p.tool)),
            toolC=toolC_formatted)
            toolC=toolC_formatted)

+ 15 - 10
postprocessors/Repetier.py → preprocessors/Repetier.py

@@ -45,7 +45,8 @@ class Repetier(FlatCAMPostProc):
         gcode += ';Z Toolchange: ' + str(p['z_toolchange']) + units + '\n'
         gcode += ';Z Toolchange: ' + str(p['z_toolchange']) + units + '\n'
 
 
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += ';X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + '\n'
+            gcode += ';X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + '\n'
         else:
         else:
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
 
 
@@ -54,9 +55,9 @@ class Repetier(FlatCAMPostProc):
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
+            gcode += ';Preprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
         else:
         else:
-            gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n'
+            gcode += ';Preprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n'
 
 
         gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n'
         gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n'
         gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n'
         gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n'
@@ -78,7 +79,7 @@ class Repetier(FlatCAMPostProc):
         return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
         return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
 
 
     def down_code(self, p):
     def down_code(self, p):
-        return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.end_feedrate_code(p)
+        return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.inline_z_feedrate_code(p)
 
 
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         z_toolchange = p.z_toolchange
         z_toolchange = p.z_toolchange
@@ -95,10 +96,7 @@ class Repetier(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
@@ -111,6 +109,7 @@ G0 Z{z_toolchange}
 G0 X{x_toolchange} Y{y_toolchange}                
 G0 X{x_toolchange} Y{y_toolchange}                
 M84
 M84
 @pause Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
 @pause Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
+G0 Z{z_toolchange}
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -122,6 +121,7 @@ M84
 G0 Z{z_toolchange}
 G0 Z{z_toolchange}
 M84
 M84
 @pause Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
 @pause Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
+G0 Z{z_toolchange}
 """.format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
 """.format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            tool=int(p.tool),
            tool=int(p.tool),
            t_drills=no_drills,
            t_drills=no_drills,
@@ -139,6 +139,7 @@ G0 Z{z_toolchange}
 G0 X{x_toolchange} Y{y_toolchange}
 G0 X{x_toolchange} Y{y_toolchange}
 M84
 M84
 @pause Change to tool T{tool} with Tool Dia = {toolC}
 @pause Change to tool T{tool} with Tool Dia = {toolC}
+G0 Z{z_toolchange}
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -149,6 +150,7 @@ M84
 G0 Z{z_toolchange}
 G0 Z{z_toolchange}
 M84
 M84
 @pause Change to tool T{tool} with Tool Dia = {toolC}
 @pause Change to tool T{tool} with Tool Dia = {toolC}
+G0 Z{z_toolchange}
 """.format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
 """.format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
            tool=int(p.tool),
            tool=int(p.tool),
            toolC=toolC_formatted)
            toolC=toolC_formatted)
@@ -169,7 +171,7 @@ M84
         return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
         return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
 
 
     def linear_code(self, p):
     def linear_code(self, p):
-        return ('G1 ' + self.position_code(p)).format(**p) + " " + self.end_feedrate_code(p)
+        return ('G1 ' + self.position_code(p)).format(**p) + " " + self.inline_feedrate_code(p)
 
 
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['xy_toolchange']
         coords_xy = p['xy_toolchange']
@@ -183,9 +185,12 @@ M84
     def feedrate_code(self, p):
     def feedrate_code(self, p):
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
 
 
-    def end_feedrate_code(self, p):
+    def inline_feedrate_code(self, p):
         return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
         return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
 
 
+    def inline_z_feedrate_code(self, p):
+        return 'F' + self.feedrate_format %(p.fr_decimals, p.z_feedrate)
+
     def z_feedrate_code(self, p):
     def z_feedrate_code(self, p):
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.z_feedrate))
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.z_feedrate))
 
 

+ 1 - 1
postprocessors/Roland_MDX_20.py → preprocessors/Roland_MDX_20.py

@@ -9,7 +9,7 @@
 from FlatCAMPostProc import *
 from FlatCAMPostProc import *
 
 
 
 
-# for Roland Postprocessors it is mandatory for the postprocessor name (python file and class name, both of them must be
+# for Roland Preprocessors it is mandatory for the preprocessor name (python file and class name, both of them must be
 # the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
 # the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
 class Roland_MDX_20(FlatCAMPostProc):
 class Roland_MDX_20(FlatCAMPostProc):
 
 

+ 5 - 7
postprocessors/Toolchange_Custom.py → preprocessors/Toolchange_Custom.py

@@ -44,7 +44,8 @@ class Toolchange_Custom(FlatCAMPostProc):
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
 
 
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
 
 
@@ -53,9 +54,9 @@ class Toolchange_Custom(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -93,10 +94,7 @@ class Toolchange_Custom(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:

+ 5 - 7
postprocessors/Toolchange_Probe_MACH3.py → preprocessors/Toolchange_Probe_MACH3.py

@@ -45,7 +45,8 @@ class Toolchange_Probe_MACH3(FlatCAMPostProc):
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
 
 
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
 
 
@@ -55,9 +56,9 @@ class Toolchange_Probe_MACH3(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -99,10 +100,7 @@ class Toolchange_Probe_MACH3(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:

+ 5 - 10
postprocessors/Toolchange_manual.py → preprocessors/Toolchange_manual.py

@@ -43,7 +43,8 @@ class Toolchange_manual(FlatCAMPostProc):
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
@@ -51,9 +52,9 @@ class Toolchange_manual(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -88,19 +89,13 @@ class Toolchange_manual(FlatCAMPostProc):
         if toolchangexy is not None:
         if toolchangexy is not None:
             x_toolchange = toolchangexy[0]
             x_toolchange = toolchangexy[0]
             y_toolchange = toolchangexy[1]
             y_toolchange = toolchangexy[1]
-        # else:
-        #     x_toolchange = p.oldx
-        #     y_toolchange = p.oldy
 
 
         no_drills = 1
         no_drills = 1
 
 
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:

+ 0 - 0
postprocessors/__init__.py → preprocessors/__init__.py


+ 27 - 20
postprocessors/default.py → preprocessors/default.py

@@ -44,7 +44,8 @@ class default(FlatCAMPostProc):
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
 
 
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
 
 
@@ -53,9 +54,9 @@ class default(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -95,10 +96,7 @@ class default(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
@@ -109,11 +107,12 @@ class default(FlatCAMPostProc):
                 gcode = """
                 gcode = """
 M5
 M5
 G00 Z{z_toolchange}
 G00 Z{z_toolchange}
-G00 X{x_toolchange} Y{y_toolchange}                
 T{tool}
 T{tool}
+G00 X{x_toolchange} Y{y_toolchange}                
 M6
 M6
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 M0
 M0
+G00 Z{z_toolchange}
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
 """.format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
              y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
              y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
              z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
              z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -127,10 +126,13 @@ G00 Z{z_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
-M0""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             t_drills=no_drills,
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
             return gcode
             return gcode
@@ -144,11 +146,14 @@ G00 X{x_toolchange} Y{y_toolchange}
 T{tool}
 T{tool}
 M6    
 M6    
 (MSG, Change to Tool Dia = {toolC})
 (MSG, Change to Tool Dia = {toolC})
-M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
-             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
-             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+
             else:
             else:
                 gcode = """
                 gcode = """
 M5
 M5
@@ -156,9 +161,11 @@ G00 Z{z_toolchange}
 T{tool}
 T{tool}
 M6    
 M6    
 (MSG, Change to Tool Dia = {toolC})
 (MSG, Change to Tool Dia = {toolC})
-M0""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
 
 
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)

+ 31 - 25
postprocessors/grbl_11.py → preprocessors/grbl_11.py

@@ -43,7 +43,8 @@ class grbl_11(FlatCAMPostProc):
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
@@ -51,9 +52,9 @@ class grbl_11(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -94,10 +95,7 @@ class grbl_11(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
@@ -112,12 +110,14 @@ G00 X{x_toolchange} Y{y_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
-M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
-             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
-             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             t_drills=no_drills,
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
             else:
             else:
                 gcode = """
                 gcode = """
 M5             
 M5             
@@ -125,10 +125,12 @@ G00 Z{z_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
-M0""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             t_drills=no_drills,
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
 
 
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
@@ -143,11 +145,13 @@ G00 X{x_toolchange} Y{y_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 (MSG, Change to Tool Dia = {toolC})
 (MSG, Change to Tool Dia = {toolC})
-M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
-             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
-             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
             else:
             else:
                 gcode = """
                 gcode = """
 M5             
 M5             
@@ -155,9 +159,11 @@ G00 Z{z_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 (MSG, Change to Tool Dia = {toolC})
 (MSG, Change to Tool Dia = {toolC})
-M0""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G00 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
 
 
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)

+ 2 - 2
postprocessors/grbl_laser.py → preprocessors/grbl_laser.py

@@ -32,9 +32,9 @@ class grbl_laser(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
         gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + '\n'
         gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'

+ 1 - 1
postprocessors/hpgl.py → preprocessors/hpgl.py

@@ -9,7 +9,7 @@
 from FlatCAMPostProc import *
 from FlatCAMPostProc import *
 
 
 
 
-# for Roland Postprocessors it is mandatory for the postprocessor name (python file and class name, both of them must be
+# for Roland Preprocessors it is mandatory for the preprocessor name (python file and class name, both of them must be
 # the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
 # the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
 class hpgl(FlatCAMPostProc):
 class hpgl(FlatCAMPostProc):
 
 

+ 5 - 7
postprocessors/line_xyz.py → preprocessors/line_xyz.py

@@ -43,7 +43,8 @@ class line_xyz(FlatCAMPostProc):
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+            gcode += '(X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + ')\n'
         else:
         else:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
@@ -51,9 +52,9 @@ class line_xyz(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
 
 
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
         gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -109,10 +110,7 @@ class line_xyz(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:

+ 39 - 30
postprocessors/marlin.py → preprocessors/marlin.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMPostProc import *
 from FlatCAMPostProc import *
 
 
@@ -45,7 +45,8 @@ class marlin(FlatCAMPostProc):
         gcode += ';Z Toolchange: ' + str(p['z_toolchange']) + units + '\n'
         gcode += ';Z Toolchange: ' + str(p['z_toolchange']) + units + '\n'
 
 
         if coords_xy is not None:
         if coords_xy is not None:
-            gcode += ';X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + '\n'
+            gcode += ';X,Y Toolchange: ' + "%.*f, %.*f" % (p.decimals, coords_xy[0],
+                                                           p.decimals, coords_xy[1]) + units + '\n'
         else:
         else:
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
 
 
@@ -54,9 +55,9 @@ class marlin(FlatCAMPostProc):
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
+            gcode += ';Preprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
         else:
         else:
-            gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n'
+            gcode += ';Preprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n'
 
 
         gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n'
         gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n'
         gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n'
         gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n'
@@ -78,7 +79,7 @@ class marlin(FlatCAMPostProc):
         return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
         return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
 
 
     def down_code(self, p):
     def down_code(self, p):
-        return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.end_feedrate_code(p)
+        return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.inline_z_feedrate_code(p)
 
 
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         z_toolchange = p.z_toolchange
         z_toolchange = p.z_toolchange
@@ -95,10 +96,7 @@ class marlin(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             z_toolchange = p.startz
             z_toolchange = p.startz
 
 
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(p.toolC, '.2f')
-        else:
-            toolC_formatted = format(p.toolC, '.4f')
+        toolC_formatted = '%.*f' % (p.decimals, p.toolC)
 
 
         if str(p['options']['type']) == 'Excellon':
         if str(p['options']['type']) == 'Excellon':
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
@@ -113,12 +111,14 @@ G0 X{x_toolchange} Y{y_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 ;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
 ;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
-M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
-             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
-             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             t_drills=no_drills,
-             toolC=toolC_formatted)
+M0
+G0 Z{z_toolchange}
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
             else:
             else:
                 gcode = """
                 gcode = """
 M5
 M5
@@ -126,10 +126,12 @@ G0 Z{z_toolchange}
 T{tool}
 T{tool}
 M6
 M6
 ;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
 ;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
-M0""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             t_drills=no_drills,
-             toolC=toolC_formatted)
+M0
+G0 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
 
 
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
@@ -144,11 +146,13 @@ G0 X{x_toolchange} Y{y_toolchange}
 T{tool}
 T{tool}
 M6    
 M6    
 ;MSG, Change to Tool Dia = {toolC}
 ;MSG, Change to Tool Dia = {toolC}
-M0""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
-             y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
-             z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G0 Z{z_toolchange}
+""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
+           y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
+           z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
             else:
             else:
                 gcode = """
                 gcode = """
 M5
 M5
@@ -156,9 +160,11 @@ G0 Z{z_toolchange}
 T{tool}
 T{tool}
 M6    
 M6    
 ;MSG, Change to Tool Dia = {toolC}
 ;MSG, Change to Tool Dia = {toolC}
-M0""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
-             tool=int(p.tool),
-             toolC=toolC_formatted)
+M0
+G0 Z{z_toolchange}
+""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchange),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
 
 
             if f_plunge is True:
             if f_plunge is True:
                 gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
                 gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
@@ -175,7 +181,7 @@ M0""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchang
         return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
         return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
 
 
     def linear_code(self, p):
     def linear_code(self, p):
-        return ('G1 ' + self.position_code(p)).format(**p) + " " + self.end_feedrate_code(p)
+        return ('G1 ' + self.position_code(p)).format(**p) + " " + self.inline_feedrate_code(p)
 
 
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['xy_toolchange']
         coords_xy = p['xy_toolchange']
@@ -189,9 +195,12 @@ M0""".format(z_toolchange=self.coordinate_format%(p.coords_decimals, z_toolchang
     def feedrate_code(self, p):
     def feedrate_code(self, p):
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
 
 
-    def end_feedrate_code(self, p):
+    def inline_feedrate_code(self, p):
         return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
         return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
 
 
+    def inline_z_feedrate_code(self, p):
+        return 'F' + self.feedrate_format %(p.fr_decimals, p.z_feedrate)
+
     def z_feedrate_code(self, p):
     def z_feedrate_code(self, p):
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.z_feedrate))
         return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.z_feedrate))
 
 

+ 6 - 4
requirements.txt

@@ -1,7 +1,7 @@
 # This file contains python only requirements to be installed with pip
 # This file contains python only requirements to be installed with pip
-# Python pacakges that cannot be installed with pip (e.g. PyQt5, GDAL) are not included.
-# Usage: pip install -r requirements.txt
-numpy>=1.11
+# Python packages that cannot be installed with pip (e.g. PyQt5, GDAL) are not included.
+# Usage: pip3 install -r requirements.txt
+numpy>=1.16
 matplotlib>=3.1
 matplotlib>=3.1
 cycler>=0.10
 cycler>=0.10
 python-dateutil>=2.1
 python-dateutil>=2.1
@@ -11,7 +11,6 @@ setuptools
 dill
 dill
 rtree
 rtree
 pyopengl
 pyopengl
-pyopengl-accelerate
 vispy
 vispy
 ortools
 ortools
 svg.path
 svg.path
@@ -22,3 +21,6 @@ fontTools
 rasterio
 rasterio
 lxml
 lxml
 ezdxf
 ezdxf
+qrcode>=6.0
+reportlab>=3.0
+svglib

+ 8 - 33
setup_ubuntu.sh

@@ -1,33 +1,8 @@
-#!/bin/sh
-apt-get install python3-pip
-apt-get install python3-pyqt5
-apt-get install python3-pyqt5.qtopengl
-apt-get install libpng-dev
-apt-get install libfreetype6 libfreetype6-dev
-apt-get install python3-dev
-apt-get install python3-simplejson
-apt-get install python3-numpy python3-scipy
-apt-get install libgeos-dev
-apt-get install python3-shapely
-apt-get install python3-rtree
-apt-get install python3-tk
-apt-get install libspatialindex-dev
-apt-get install python3-gdal
-apt-get install python3-lxml
-pip3 install --upgrade cycler
-pip3 install --upgrade python-dateutil
-pip3 install --upgrade kiwisolver
-pip3 install --upgrade dill
-pip3 install --upgrade Shapely
-pip3 install --upgrade vispy
-pip3 install --upgrade rtree
-pip3 install --upgrade pyopengl
-pip3 install --upgrade setuptools
-pip3 install --upgrade svg.path
-pip3 install --upgrade ortools
-pip3 install --upgrade freetype-py
-pip3 install --upgrade fontTools
-pip3 install --upgrade rasterio
-pip3 install --upgrade lxml
-pip3 install --upgrade ezdxf
-pip3 install --upgrade matplotlib
+#!/bin/bash
+sudo apt install --reinstall libpng-dev libfreetype6 libfreetype6-dev libgeos-dev libspatialindex-dev
+sudo apt install --reinstall python3-dev python3-pyqt5 python3-pyqt5.qtopengl python3-gdal python3-simplejson
+sudo apt install --reinstall python3-pip python3-tk python3-imaging
+
+sudo python3 -m pip install --upgrade pip numpy scipy shapely rtree tk lxml cycler python-dateutil kiwisolver dill
+sudo python3 -m pip install --upgrade vispy pyopengl setuptools svg.path ortools freetype-py fontTools rasterio ezdxf
+sudo python3 -m pip install --upgrade matplotlib qrcode reportlab svglib

BIN
share/copperfill16.png


BIN
share/copperfill32.png


BIN
share/database32.png


BIN
share/fiducials_32.png


BIN
share/qrcode32.png


+ 2 - 2
tclCommands/TclCommandAddCircle.py

@@ -1,4 +1,4 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
 
 
 
 
@@ -56,7 +56,7 @@ class TclCommandAddCircle(TclCommand):
 
 
         try:
         try:
             obj = self.app.collection.get_by_name(str(obj_name))
             obj = self.app.collection.get_by_name(str(obj_name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % obj_name
             return "Could not retrieve object: %s" % obj_name
         if obj is None:
         if obj is None:
             return "Object not found: %s" % obj_name
             return "Object not found: %s" % obj_name

+ 1 - 1
tclCommands/TclCommandAddPolygon.py

@@ -1,4 +1,4 @@
-from ObjectCollection import *
+from camlib import Geometry
 from tclCommands.TclCommand import *
 from tclCommands.TclCommand import *
 
 
 
 

+ 2 - 1
tclCommands/TclCommandAddPolyline.py

@@ -1,4 +1,5 @@
-from ObjectCollection import *
+from camlib import Geometry
+import collections
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
 
 
 
 

+ 2 - 2
tclCommands/TclCommandAddRectangle.py

@@ -1,4 +1,4 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
 
 
 
 
@@ -58,7 +58,7 @@ class TclCommandAddRectangle(TclCommandSignaled):
 
 
         try:
         try:
             obj = self.app.collection.get_by_name(str(obj_name))
             obj = self.app.collection.get_by_name(str(obj_name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % obj_name
             return "Could not retrieve object: %s" % obj_name
         if obj is None:
         if obj is None:
             return "Object not found: %s" % obj_name
             return "Object not found: %s" % obj_name

+ 9 - 3
tclCommands/TclCommandAlignDrill.py

@@ -1,5 +1,9 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
+from FlatCAMObj import FlatCAMGeometry, FlatCAMGerber, FlatCAMExcellon
+
+from shapely.geometry import Point
+import shapely.affinity as affinity
 
 
 
 
 class TclCommandAlignDrill(TclCommandSignaled):
 class TclCommandAlignDrill(TclCommandSignaled):
@@ -42,6 +46,8 @@ class TclCommandAlignDrill(TclCommandSignaled):
             ('name', 'Name of the object (Gerber or Excellon) to mirror.'),
             ('name', 'Name of the object (Gerber or Excellon) to mirror.'),
             ('dia', 'Tool diameter'),
             ('dia', 'Tool diameter'),
             ('box', 'Name of object which act as box (cutout for example.)'),
             ('box', 'Name of object which act as box (cutout for example.)'),
+            ('holes', 'Tuple of tuples where each tuple it is a set of x, y coordinates. '
+                      'E.g: (x0, y0), (x1, y1), ... '),
             ('grid', 'Aligning to grid, for those, who have aligning pins'
             ('grid', 'Aligning to grid, for those, who have aligning pins'
                      'inside table in grid (-5,0),(5,0),(15,0)...'),
                      'inside table in grid (-5,0),(5,0),(15,0)...'),
             ('gridoffset', 'offset of grid from 0 position.'),
             ('gridoffset', 'offset of grid from 0 position.'),
@@ -75,7 +81,7 @@ class TclCommandAlignDrill(TclCommandSignaled):
         # Get source object.
         # Get source object.
         try:
         try:
             obj = self.app.collection.get_by_name(str(name))
             obj = self.app.collection.get_by_name(str(name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % name
             return "Could not retrieve object: %s" % name
 
 
         if obj is None:
         if obj is None:
@@ -173,7 +179,7 @@ class TclCommandAlignDrill(TclCommandSignaled):
         if 'box' in args:
         if 'box' in args:
             try:
             try:
                 box = self.app.collection.get_by_name(args['box'])
                 box = self.app.collection.get_by_name(args['box'])
-            except:
+            except Exception:
                 return "Could not retrieve object box: %s" % args['box']
                 return "Could not retrieve object box: %s" % args['box']
 
 
             if box is None:
             if box is None:

+ 2 - 1
tclCommands/TclCommandAlignDrillGrid.py

@@ -1,6 +1,7 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
 
 
+from shapely.geometry import Point
 
 
 class TclCommandAlignDrillGrid(TclCommandSignaled):
 class TclCommandAlignDrillGrid(TclCommandSignaled):
     """
     """

+ 6 - 2
tclCommands/TclCommandBbox.py

@@ -1,5 +1,9 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
+from FlatCAMObj import FlatCAMGeometry, FlatCAMGerber
+
+from shapely.ops import cascaded_union
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -75,7 +79,7 @@ class TclCommandBbox(TclCommand):
 
 
         if 'rounded' not in args:
         if 'rounded' not in args:
             args['rounded'] = self.app.defaults["gerber_bboxrounded"]
             args['rounded'] = self.app.defaults["gerber_bboxrounded"]
-        rounded = args['rounded']
+        rounded = bool(args['rounded'])
 
 
         del args['name']
         del args['name']
 
 

+ 3 - 3
tclCommands/TclCommandBounds.py

@@ -1,7 +1,6 @@
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
-from ObjectCollection import *
-
-from camlib import get_bounds
+import collections
+import logging
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -11,6 +10,7 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
+log = logging.getLogger('base')
 
 
 class TclCommandBounds(TclCommand):
 class TclCommandBounds(TclCommand):
     """
     """

+ 1 - 1
tclCommands/TclCommandClearShell.py

@@ -6,7 +6,7 @@
 # ##########################################################
 # ##########################################################
 
 
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
-from ObjectCollection import *
+import collections
 
 
 
 
 class TclCommandClearShell(TclCommand):
 class TclCommandClearShell(TclCommand):

+ 9 - 7
tclCommands/TclCommandCncjob.py

@@ -1,5 +1,8 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
+from FlatCAMObj import FlatCAMGeometry
+
+import collections
+from copy import deepcopy
 
 
 
 
 class TclCommandCncjob(TclCommandSignaled):
 class TclCommandCncjob(TclCommandSignaled):
@@ -60,7 +63,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('feedrate', 'Moving speed on X-Y plane when cutting.'),
             ('feedrate', 'Moving speed on X-Y plane when cutting.'),
             ('feedrate_z', 'Moving speed on Z plane when cutting.'),
             ('feedrate_z', 'Moving speed on Z plane when cutting.'),
             ('feedrate_rapid', 'Rapid moving at speed when cutting.'),
             ('feedrate_rapid', 'Rapid moving at speed when cutting.'),
-            ('multidepth', 'Use or not multidepth cnccut. (True or False)'),
+            ('multidepth', 'Use or not multidepth cnc cut. (True or False)'),
             ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'),
             ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('toolchange', 'Enable tool changes (example: True).'),
             ('toolchange', 'Enable tool changes (example: True).'),
@@ -72,7 +75,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('dwell', 'True or False; use (or not) the dwell'),
             ('dwell', 'True or False; use (or not) the dwell'),
             ('dwelltime', 'Time to pause to allow the spindle to reach the full speed'),
             ('dwelltime', 'Time to pause to allow the spindle to reach the full speed'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('outname', 'Name of the resulting Geometry object.'),
-            ('pp', 'Name of the Geometry postprocessor. No quotes, case sensitive'),
+            ('pp', 'Name of the Geometry preprocessor. No quotes, case sensitive'),
             ('muted', 'It will not put errors in the Shell.')
             ('muted', 'It will not put errors in the Shell.')
         ]),
         ]),
         'examples': ['cncjob geo_name -dia 0.5 -z_cut -1.7 -z_move 2 -feedrate 120 -pp default']
         'examples': ['cncjob geo_name -dia 0.5 -z_cut -1.7 -z_move 2 -feedrate 120 -pp default']
@@ -131,9 +134,8 @@ class TclCommandCncjob(TclCommandSignaled):
         args["feedrate_rapid"] = args["feedrate_rapid"] if "feedrate_rapid" in args and args["feedrate_rapid"] else \
         args["feedrate_rapid"] = args["feedrate_rapid"] if "feedrate_rapid" in args and args["feedrate_rapid"] else \
             obj.options["feedrate_rapid"]
             obj.options["feedrate_rapid"]
 
 
-        args["multidepth"] = args["multidepth"] if "multidepth" in args and args["multidepth"] else \
-            obj.options["multidepth"]
-        args["extracut"] = args["extracut"] if "extracut" in args and args["extracut"] else obj.options["extracut"]
+        args["multidepth"] = bool(args["multidepth"]) if "multidepth" in args else obj.options["multidepth"]
+        args["extracut"] = bool(args["extracut"]) if "extracut" in args else obj.options["extracut"]
         args["depthperpass"] = args["depthperpass"] if "depthperpass" in args and args["depthperpass"] else \
         args["depthperpass"] = args["depthperpass"] if "depthperpass" in args and args["depthperpass"] else \
             obj.options["depthperpass"]
             obj.options["depthperpass"]
 
 
@@ -142,7 +144,7 @@ class TclCommandCncjob(TclCommandSignaled):
         args["endz"] = args["endz"] if "endz" in args and args["endz"] else obj.options["endz"]
         args["endz"] = args["endz"] if "endz" in args and args["endz"] else obj.options["endz"]
 
 
         args["spindlespeed"] = args["spindlespeed"] if "spindlespeed" in args and args["spindlespeed"] else None
         args["spindlespeed"] = args["spindlespeed"] if "spindlespeed" in args and args["spindlespeed"] else None
-        args["dwell"] = args["dwell"] if "dwell" in args and args["dwell"] else obj.options["dwell"]
+        args["dwell"] = bool(args["dwell"]) if "dwell" in args else obj.options["dwell"]
         args["dwelltime"] = args["dwelltime"] if "dwelltime" in args and args["dwelltime"] else obj.options["dwelltime"]
         args["dwelltime"] = args["dwelltime"] if "dwelltime" in args and args["dwelltime"] else obj.options["dwelltime"]
 
 
         args["pp"] = args["pp"] if "pp" in args and args["pp"] else obj.options["ppname_g"]
         args["pp"] = args["pp"] if "pp" in args and args["pp"] else obj.options["ppname_g"]

+ 21 - 17
tclCommands/TclCommandCopperClear.py

@@ -1,6 +1,8 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
 
 
+import collections
+import logging
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -9,6 +11,8 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
+log = logging.getLogger('base')
+
 
 
 class TclCommandCopperClear(TclCommand):
 class TclCommandCopperClear(TclCommand):
     """
     """
@@ -85,6 +89,17 @@ class TclCommandCopperClear(TclCommand):
 
 
         name = args['name']
         name = args['name']
 
 
+        # Get source object.
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except Exception as e:
+            log.debug("TclCommandCopperClear.execute() --> %s" % str(e))
+            self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name))
+            return "Could not retrieve object: %s" % name
+
+        if obj is None:
+            return "Object not found: %s" % name
+
         if 'tooldia' in args:
         if 'tooldia' in args:
             tooldia = str(args['tooldia'])
             tooldia = str(args['tooldia'])
         else:
         else:
@@ -111,18 +126,18 @@ class TclCommandCopperClear(TclCommand):
             method = str(self.app.defaults["tools_nccmethod"])
             method = str(self.app.defaults["tools_nccmethod"])
 
 
         if 'connect' in args:
         if 'connect' in args:
-            connect = eval(str(args['connect']).capitalize())
+            connect = bool(args['connect'])
         else:
         else:
             connect = eval(str(self.app.defaults["tools_nccconnect"]))
             connect = eval(str(self.app.defaults["tools_nccconnect"]))
 
 
         if 'contour' in args:
         if 'contour' in args:
-            contour = eval(str(args['contour']).capitalize())
+            contour = bool(args['contour'])
         else:
         else:
             contour = eval(str(self.app.defaults["tools_ncccontour"]))
             contour = eval(str(self.app.defaults["tools_ncccontour"]))
 
 
         offset = 0.0
         offset = 0.0
         if 'has_offset' in args:
         if 'has_offset' in args:
-            has_offset = args['has_offset']
+            has_offset = bool(args['has_offset'])
             if args['has_offset'] is True:
             if args['has_offset'] is True:
                 if 'offset' in args:
                 if 'offset' in args:
                     offset = float(args['margin'])
                     offset = float(args['margin'])
@@ -177,7 +192,7 @@ class TclCommandCopperClear(TclCommand):
             tooluid += 1
             tooluid += 1
             ncc_tools.update({
             ncc_tools.update({
                 int(tooluid): {
                 int(tooluid): {
-                    'tooldia': float('%.4f' % tool),
+                    'tooldia': float('%.*f' % (obj.decimals, tool)),
                     'offset': 'Path',
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': 'Iso',
                     'type': 'Iso',
@@ -188,7 +203,7 @@ class TclCommandCopperClear(TclCommand):
             })
             })
 
 
         if 'rest' in args:
         if 'rest' in args:
-            rest = eval(str(args['rest']).capitalize())
+            rest = bool(args['rest'])
         else:
         else:
             rest = eval(str(self.app.defaults["tools_nccrest"]))
             rest = eval(str(self.app.defaults["tools_nccrest"]))
 
 
@@ -200,17 +215,6 @@ class TclCommandCopperClear(TclCommand):
             else:
             else:
                 outname = name + "_ncc_rm"
                 outname = name + "_ncc_rm"
 
 
-        # Get source object.
-        try:
-            obj = self.app.collection.get_by_name(str(name))
-        except Exception as e:
-            log.debug("TclCommandCopperClear.execute() --> %s" % str(e))
-            self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name))
-            return "Could not retrieve object: %s" % name
-
-        if obj is None:
-            return "Object not found: %s" % name
-
         # Non-Copper clear all polygons in the non-copper clear object
         # Non-Copper clear all polygons in the non-copper clear object
         if 'all' in args and args['all'] == 1:
         if 'all' in args and args['all'] == 1:
             self.app.ncclear_tool.clear_copper(ncc_obj=obj,
             self.app.ncclear_tool.clear_copper(ncc_obj=obj,

+ 9 - 4
tclCommands/TclCommandCutout.py

@@ -1,8 +1,13 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
+
+import collections
+import logging
+
 from shapely.ops import cascaded_union
 from shapely.ops import cascaded_union
 from shapely.geometry import LineString
 from shapely.geometry import LineString
 
 
+log = logging.getLogger('base')
+
 
 
 class TclCommandCutout(TclCommand):
 class TclCommandCutout(TclCommand):
     """
     """
@@ -62,12 +67,12 @@ class TclCommandCutout(TclCommand):
             return
             return
 
 
         if 'margin' in args:
         if 'margin' in args:
-            margin_par = args['margin']
+            margin_par = float(args['margin'])
         else:
         else:
             margin_par = 0.001
             margin_par = 0.001
 
 
         if 'dia' in args:
         if 'dia' in args:
-            dia_par = args['dia']
+            dia_par = float(args['dia'])
         else:
         else:
             dia_par = 0.1
             dia_par = 0.1
 
 
@@ -77,7 +82,7 @@ class TclCommandCutout(TclCommand):
             gaps_par = "4"
             gaps_par = "4"
 
 
         if 'gapsize' in args:
         if 'gapsize' in args:
-            gapsize_par = args['gapsize']
+            gapsize_par = float(args['gapsize'])
         else:
         else:
             gapsize_par = 0.1
             gapsize_par = 0.1
 
 

+ 2 - 1
tclCommands/TclCommandDelete.py

@@ -1,6 +1,7 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
 
 
+import collections
+
 
 
 class TclCommandDelete(TclCommand):
 class TclCommandDelete(TclCommand):
     """
     """

+ 16 - 14
tclCommands/TclCommandDrillcncjob.py

@@ -1,5 +1,8 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommandSignaled
 from tclCommands.TclCommand import TclCommandSignaled
+from FlatCAMObj import FlatCAMExcellon
+
+import collections
+import math
 
 
 
 
 class TclCommandDrillcncjob(TclCommandSignaled):
 class TclCommandDrillcncjob(TclCommandSignaled):
@@ -57,7 +60,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('dwell', 'True or False; use (or not) the dwell'),
             ('dwell', 'True or False; use (or not) the dwell'),
             ('dwelltime', 'Time to pause to allow the spindle to reach the full speed'),
             ('dwelltime', 'Time to pause to allow the spindle to reach the full speed'),
-            ('pp', 'This is the Excellon postprocessor name: case_sensitive, no_quotes'),
+            ('pp', 'This is the Excellon preprocessor name: case_sensitive, no_quotes'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('opt_type', 'Name of move optimization type. B by default for Basic OR-Tools, M for Metaheuristic OR-Tools'
             ('opt_type', 'Name of move optimization type. B by default for Basic OR-Tools, M for Metaheuristic OR-Tools'
                          'T from Travelling Salesman Algorithm. B and M works only for 64bit version of FlatCAM and '
                          'T from Travelling Salesman Algorithm. B and M works only for 64bit version of FlatCAM and '
@@ -86,17 +89,18 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
 
         name = args['name']
         name = args['name']
 
 
+        obj = self.app.collection.get_by_name(name)
+
         if 'outname' not in args:
         if 'outname' not in args:
             args['outname'] = name + "_cnc"
             args['outname'] = name + "_cnc"
 
 
         if 'muted' in args:
         if 'muted' in args:
-            muted = args['muted']
+            muted = bool(args['muted'])
         else:
         else:
-            muted = 0
+            muted = False
 
 
-        obj = self.app.collection.get_by_name(name)
         if obj is None:
         if obj is None:
-            if muted == 0:
+            if muted is False:
                 self.raise_tcl_error("Object not found: %s" % name)
                 self.raise_tcl_error("Object not found: %s" % name)
             else:
             else:
                 return "fail"
                 return "fail"
@@ -114,20 +118,18 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
 
         def job_init(job_obj, app_obj):
         def job_init(job_obj, app_obj):
             # tools = args["tools"] if "tools" in args else 'all'
             # tools = args["tools"] if "tools" in args else 'all'
-            units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+            units = self.app.defaults['units'].upper()
 
 
             try:
             try:
                 if 'drilled_dias' in args and args['drilled_dias'] != 'all':
                 if 'drilled_dias' in args and args['drilled_dias'] != 'all':
-                    diameters = [x.strip() for x in args['drilled_dias'].split(",") if x!= '']
+                    diameters = [x.strip() for x in args['drilled_dias'].split(",") if x != '']
                     nr_diameters = len(diameters)
                     nr_diameters = len(diameters)
 
 
                     req_tools = set()
                     req_tools = set()
                     for tool in obj.tools:
                     for tool in obj.tools:
                         for req_dia in diameters:
                         for req_dia in diameters:
-                            obj_dia_form = float('%.2f' % float(obj.tools[tool]["C"])) if units == 'MM' else \
-                                float('%.4f' % float(obj.tools[tool]["C"]))
-                            req_dia_form = float('%.2f' % float(req_dia)) if units == 'MM' else \
-                                float('%.4f' % float(req_dia))
+                            obj_dia_form = float('%.*f' % (obj.decimals, float(obj.tools[tool]["C"])))
+                            req_dia_form = float('%.*f' % (obj.decimals, float(req_dia)))
 
 
                             if 'diatol' in args:
                             if 'diatol' in args:
                                 tolerance = args['diatol'] / 100
                                 tolerance = args['diatol'] / 100
@@ -173,7 +175,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             toolchangez = args["toolchangez"] if "toolchangez" in args and args["toolchangez"] else \
             toolchangez = args["toolchangez"] if "toolchangez" in args and args["toolchangez"] else \
                 obj.options["toolchangez"]
                 obj.options["toolchangez"]
             endz = args["endz"] if "endz" in args and args["endz"] else obj.options["endz"]
             endz = args["endz"] if "endz" in args and args["endz"] else obj.options["endz"]
-            toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
+            toolchange = True if "toolchange" in args and bool(args["toolchange"]) is True else False
             opt_type = args["opt_type"] if "opt_type" in args and args["opt_type"] else 'B'
             opt_type = args["opt_type"] if "opt_type" in args and args["opt_type"] else 'B'
 
 
             job_obj.z_move = args["travelz"] if "travelz" in args and args["travelz"] else obj.options["travelz"]
             job_obj.z_move = args["travelz"] if "travelz" in args and args["travelz"] else obj.options["travelz"]
@@ -181,7 +183,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             job_obj.feedrate_rapid = args["feedrate_rapid"] \
             job_obj.feedrate_rapid = args["feedrate_rapid"] \
                 if "feedrate_rapid" in args and args["feedrate_rapid"] else obj.options["feedrate_rapid"]
                 if "feedrate_rapid" in args and args["feedrate_rapid"] else obj.options["feedrate_rapid"]
 
 
-            if args['dwell'] and args['dwelltime']:
+            if bool(args['dwell']) and args['dwelltime']:
                 job_obj.dwell = True
                 job_obj.dwell = True
                 job_obj.dwelltime = float(args['dwelltime'])
                 job_obj.dwelltime = float(args['dwelltime'])
 
 

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików