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            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # 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):
@@ -69,3 +92,1237 @@ class FCSignal:
         except ValueError:
             print('Warning: function %s not removed '
                   '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
 import math
 
-# module-root dictionary of postprocessors
+# module-root dictionary of preprocessors
 import FlatCAMApp
 
-postprocessors = {}
+preprocessors = {}
 
 
 class ABCPostProcRegister(ABCMeta):
-    # handles postprocessors registration on instantation
+    # handles preprocessors registration on instantiation
     def __new__(cls, clsname, bases, attrs):
         newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
         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
 
 
@@ -144,14 +144,14 @@ class FlatCAMPostProc_Tools(object, metaclass=ABCPostProcRegister):
         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
-    for path_search in postprocessors_path_search:
+    for path_search in preprocessors_path_search:
         for file in glob.glob(path_search):
             try:
                 SourceFileLoader('FlatCAMPostProcessor', file).load_module()
             except Exception as 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.QtCore import Qt
 
+from shapely.geometry import Polygon
+
 
 class FlatCAMTool(QtWidgets.QWidget):
 
@@ -22,15 +24,15 @@ class FlatCAMTool(QtWidgets.QWidget):
         :param parent: Qt Parent
         :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.layout = QtWidgets.QVBoxLayout()
         self.setLayout(self.layout)
 
-        self.app = app
-
         self.menuAction = None
 
     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.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
-# import traceback
+import traceback
 
 
 class Worker(QtCore.QObject):
@@ -61,7 +61,7 @@ class Worker(QtCore.QObject):
                 task['fcn'](*task['params'])
             except Exception as e:
                 self.app.thread_exception.emit(e)
-                # print(traceback.format_exc())
+                print(traceback.format_exc())
                 # raise e
             finally:
                 self.task_completed.emit(self.name)

+ 7 - 3
ObjectCollection.py

@@ -219,6 +219,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         "document": "share/notes16_1.png"
     }
 
+    # will emit the name of the object that was just selected
+    item_selected = QtCore.pyqtSignal(str)
+
     root_item = None
     # app = None
 
@@ -495,7 +498,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                 name += "_1"
         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)
         group = self.group_items[obj.kind]
@@ -779,6 +782,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
         try:
             obj = current.indexes()[0].internalPointer().obj
+            self.item_selected.emit(obj.options['name'])
 
             if obj.kind == 'gerber':
                 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(
                     color='darkCyan', name=str(obj.options['name'])))
         except IndexError:
+            self.item_selected.emit('none')
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             self.app.inform.emit('')
             try:
@@ -826,8 +831,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             try:
                 a_idx.build_ui()
             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
 
     def get_list(self):

+ 337 - 1
README.md

@@ -1,7 +1,7 @@
 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.
 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
 
 - 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 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 flatcamParsers.ParseExcellon import Excellon
 import FlatCAMApp
@@ -63,7 +63,7 @@ class FCDrillAdd(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_drill.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -105,7 +105,7 @@ class FCDrillAdd(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
 
         # 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:
             new_dia = self.draw_app.resdrill_entry.get_value()
-        except:
+        except Exception:
             self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
                                           _("Resize drill(s) failed. Please enter a diameter for resize."))
             return
@@ -1445,8 +1445,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app = app
         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
-        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()
         # ## Box for custom widgets
@@ -1518,6 +1521,8 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         grid1 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
 
         addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('Tool Dia'))
         addtool_entry_lbl.setToolTip(
@@ -1525,8 +1530,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
 
         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)
 
         self.addtool_btn = QtWidgets.QPushButton(_('Add Tool'))
@@ -1579,7 +1586,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         grid3.addWidget(res_entry_lbl, 0, 0)
 
         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)
 
         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.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_linear_frame = QtWidgets.QFrame()
@@ -1668,7 +1679,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         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)
 
         # Linear Drill Array angle
@@ -1676,15 +1690,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.linear_angle_label.setToolTip(
            _("Angle at which the linear array is placed.\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.")
         )
         self.linear_angle_label.setMinimumWidth(100)
 
         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.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.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.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.array_circular_frame.hide()
@@ -1754,7 +1772,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         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)
 
         # Slot direction
@@ -1777,15 +1799,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_angle_label.setToolTip(
            _("Angle at which the slot is placed.\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.")
         )
         self.slot_angle_label.setMinimumWidth(100)
 
         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.setRange(-359.99, 360.00)
+        self.slot_angle_spinner.setRange(-360.00, 360.00)
         self.slot_angle_spinner.setSingleStep(1.0)
         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.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_linear_frame = QtWidgets.QFrame()
@@ -1870,7 +1894,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         )
         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)
 
         # Linear Slot Array angle
@@ -1878,15 +1906,15 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.slot_array_linear_angle_label.setToolTip(
             _("Angle at which the linear array is placed.\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.")
         )
         self.slot_array_linear_angle_label.setMinimumWidth(100)
 
         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.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_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.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_linear_angle_spinner.hide()
@@ -2050,9 +2082,6 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.complete = False
 
-        # Number of decimals used by tools in this class
-        self.decimals = 4
-
         def make_callback(thetool):
             def f():
                 self.on_tool_select(thetool)
@@ -2070,7 +2099,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             "corner_snap": False,
             "grid_gap_link": True
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
         for option in self.options:
             if option in self.app.options:
@@ -2121,7 +2150,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
     def set_ui(self):
         # 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":
             self.decimals = 4
@@ -2220,7 +2249,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             pass
 
         # 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)
         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
         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):
         """
@@ -3212,7 +3242,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("There are no Tools definitions in the file. Aborting Excellon creation.")
                                      )
-            except:
+            except Exception:
                 msg = '[ERROR] %s' % \
                       _("An internal error has ocurred. See Shell.\n")
                 msg += traceback.format_exc()
@@ -3501,7 +3531,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                         self.app.ui.popMenu.popup(self.app.cursor.pos())
 
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FlatCAMExcEditor.on_exc_click_release() RMB click --> Error: %s" % str(e))
             raise
 
         # 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:
                         self.replot()
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FlatCAMExcEditor.on_exc_click_release() LMB click --> Error: %s" % str(e))
             raise
 
     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)
 
         self.draw_app = draw_app
+        self.decimals = app.decimals
 
         # Title
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
@@ -80,7 +81,7 @@ class BufferSelectionTool(FlatCAMTool):
 
         # Buffer distance
         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)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
@@ -199,6 +200,7 @@ class TextInputTool(FlatCAMTool):
 
         self.app = app
         self.text_path = []
+        self.decimals = self.app.decimals
 
         self.f_parse = ParseFont(self)
         self.f_parse.get_fonts_by_types()
@@ -367,7 +369,7 @@ class TextInputTool(FlatCAMTool):
                     font_name=self.font_name,
                     font_size=font_to_geo_size,
                     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):
         self.text_input_entry.selectAll()
@@ -418,6 +420,7 @@ class PaintOptionsTool(FlatCAMTool):
 
         self.app = app
         self.fcdraw = fcdraw
+        self.decimals = self.app.decimals
 
         # Title
         title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
@@ -432,6 +435,8 @@ class PaintOptionsTool(FlatCAMTool):
 
         grid = QtWidgets.QGridLayout()
         self.layout.addLayout(grid)
+        grid.setColumnStretch(0, 0)
+        grid.setColumnStretch(1, 1)
 
         # Tool dia
         ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
@@ -441,7 +446,9 @@ class PaintOptionsTool(FlatCAMTool):
         )
         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)
 
         # Overlap
@@ -453,13 +460,17 @@ class PaintOptionsTool(FlatCAMTool):
               "Adjust the value starting with lower values\n"
               "and increasing it if areas that should be painted are still \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"
               "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)
-        self.paintoverlap_entry = FCEntry()
-        self.paintoverlap_entry.setValidator(QtGui.QDoubleValidator(0.0000, 1.0000, 4))
         grid.addWidget(self.paintoverlap_entry, 1, 1)
 
         # Margin
@@ -469,8 +480,11 @@ class PaintOptionsTool(FlatCAMTool):
              "the edges of the polygon to\n"
              "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)
-        self.paintmargin_entry = FCEntry()
         grid.addWidget(self.paintmargin_entry, 2, 1)
 
         # Method
@@ -480,12 +494,13 @@ class PaintOptionsTool(FlatCAMTool):
               "<B>Standard</B>: Fixed step inwards.<BR>"
               "<B>Seed-based</B>: Outwards from seed.")
         )
-        grid.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
             {"label": _("Standard"), "value": "standard"},
             {"label": _("Seed-based"), "value": "seed"},
             {"label": _("Straight lines"), "value": "lines"}
         ], orientation='vertical', stretch=False)
+
+        grid.addWidget(methodlabel, 3, 0)
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
         # Connect lines
@@ -494,8 +509,9 @@ class PaintOptionsTool(FlatCAMTool):
            _("Draw lines between resulting\n"
              "segments to minimize tool lifts.")
         )
-        grid.addWidget(pathconnectlabel, 4, 0)
         self.pathconnect_cb = FCCheckBox()
+
+        grid.addWidget(pathconnectlabel, 4, 0)
         grid.addWidget(self.pathconnect_cb, 4, 1)
 
         contourlabel = QtWidgets.QLabel(_("Contour:"))
@@ -503,8 +519,9 @@ class PaintOptionsTool(FlatCAMTool):
             _("Cut around the perimeter of the polygon\n"
               "to trim rough edges.")
         )
-        grid.addWidget(contourlabel, 5, 0)
         self.paintcontour_cb = FCCheckBox()
+
+        grid.addWidget(contourlabel, 5, 0)
         grid.addWidget(self.paintcontour_cb, 5, 1)
 
         # Buttons
@@ -569,40 +586,10 @@ class PaintOptionsTool(FlatCAMTool):
                                  _("Paint cancelled. No shape selected."))
             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()
         contour = self.paintcontour_cb.get_value()
         connect = self.pathconnect_cb.get_value()
@@ -632,6 +619,7 @@ class TransformEditorTool(FlatCAMTool):
 
         self.app = app
         self.draw_app = draw_app
+        self.decimals = self.app.decimals
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
@@ -678,9 +666,11 @@ class TransformEditorTool(FlatCAMTool):
         )
         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.set_value(_("Rotate"))
@@ -714,9 +704,11 @@ class TransformEditorTool(FlatCAMTool):
             "Float number between -360 and 359.")
         )
         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.set_value(_("Skew X"))
@@ -732,9 +724,11 @@ class TransformEditorTool(FlatCAMTool):
              "Float number between -360 and 359.")
         )
         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.set_value(_("Skew Y"))
@@ -770,9 +764,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Factor for Scale action over X axis.")
         )
         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.set_value(_("Scale X"))
@@ -787,9 +783,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Factor for Scale action over Y axis.")
         )
         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.set_value(_("Scale Y"))
@@ -844,9 +842,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Value for Offset action on X axis.")
         )
         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.set_value(_("Offset X"))
@@ -862,9 +862,11 @@ class TransformEditorTool(FlatCAMTool):
             _("Value for Offset action on Y axis.")
         )
         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.set_value(_("Offset Y"))
@@ -903,7 +905,6 @@ class TransformEditorTool(FlatCAMTool):
             _("Flip the selected shape(s) over the X axis.\n"
               "Does not create a new shape.")
         )
-        self.flipx_button.setFixedWidth(60)
 
         self.flipy_button = FCButton()
         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"
               "Does not create a new shape.")
         )
-        self.flipy_button.setFixedWidth(60)
 
         self.flip_ref_cb = FCCheckBox()
         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.")
         )
         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.set_value(_("Add"))
@@ -949,7 +947,6 @@ class TransformEditorTool(FlatCAMTool):
            )
         self.flip_ref_button.setFixedWidth(60)
 
-        form4_child_hlay.addStretch()
         form4_child_hlay.addWidget(self.flipx_button)
         form4_child_hlay.addWidget(self.flipy_button)
 
@@ -1297,8 +1294,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
 
                 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
 
     def on_flip(self, axis):
@@ -1358,8 +1354,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
 
                 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
 
     def on_skew(self, axis, num):
@@ -1405,8 +1400,7 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
 
                 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
 
     def on_scale(self, axis, xfactor, yfactor, point=None):
@@ -1462,8 +1456,7 @@ class TransformEditorTool(FlatCAMTool):
                                              _('Scale on the Y axis done'))
                     self.app.progress.emit(100)
                 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
 
     def on_offset(self, axis, num):
@@ -1496,14 +1489,13 @@ class TransformEditorTool(FlatCAMTool):
                     self.app.progress.emit(100)
 
                 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
 
     def on_rotate_key(self):
         val_box = FCInputDialog(title=_("Rotate ..."),
                                 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']))
         val_box.setWindowIcon(QtGui.QIcon('share/rotate.png'))
 
@@ -1518,11 +1510,11 @@ class TransformEditorTool(FlatCAMTool):
                                  _("Geometry shape rotate cancelled"))
 
     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 ..."),
                                 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']))
         val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png'))
 
@@ -1537,11 +1529,11 @@ class TransformEditorTool(FlatCAMTool):
                                  _("Geometry shape offset X cancelled"))
 
     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 ..."),
                                 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']))
         val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png'))
 
@@ -1558,7 +1550,7 @@ class TransformEditorTool(FlatCAMTool):
     def on_skewx_key(self):
         val_box = FCInputDialog(title=_("Skew on X axis ..."),
                                 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']))
         val_box.setWindowIcon(QtGui.QIcon('share/skewX.png'))
 
@@ -1575,7 +1567,7 @@ class TransformEditorTool(FlatCAMTool):
     def on_skewy_key(self):
         val_box = FCInputDialog(title=_("Skew on Y axis ..."),
                                 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']))
         val_box.setWindowIcon(QtGui.QIcon('share/skewY.png'))
 
@@ -1815,7 +1807,7 @@ class DrawToolShape(object):
 
         try:
             xfactor = float(xfactor)
-        except:
+        except Exception:
             log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
             return
 
@@ -1824,7 +1816,7 @@ class DrawToolShape(object):
         else:
             try:
                 yfactor = float(yfactor)
-            except:
+            except Exception:
                 log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.")
                 return
 
@@ -1946,7 +1938,7 @@ class FCCircle(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -1979,7 +1971,7 @@ class FCCircle(FCShapeTool):
     def make(self):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
 
         p1 = self.points[0]
@@ -1998,7 +1990,7 @@ class FCArc(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2217,7 +2209,7 @@ class FCRectangle(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2248,7 +2240,7 @@ class FCRectangle(FCShapeTool):
     def make(self):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
 
         p1 = self.points[0]
@@ -2271,7 +2263,7 @@ class FCPolygon(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2304,7 +2296,7 @@ class FCPolygon(FCShapeTool):
     def make(self):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
 
         # self.geometry = LinearRing(self.points)
@@ -2334,7 +2326,7 @@ class FCPath(FCPolygon):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_path5.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2694,7 +2686,7 @@ class FCText(FCShapeTool):
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception as e:
+        except Exception:
             pass
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_text.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -2748,7 +2740,7 @@ class FCText(FCShapeTool):
 
         try:
             return DrawToolUtilityShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
-        except:
+        except Exception:
             return
 
 
@@ -3033,6 +3025,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.app = app
         self.canvas = app.plotcanvas
+        self.decimals = app.decimals
 
         # ## Toolbar events and properties
         self.tools = {
@@ -3139,7 +3132,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             "corner_snap": False,
             "grid_gap_link": True
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
         for option in self.options:
             if option in self.app.options:
@@ -3152,9 +3145,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.rtree_index = rtindex.Index()
 
-        # Number of decimals used by tools in this class
-        self.decimals = 4
-
         def entry2option(option, entry):
             try:
                 self.options[option] = float(entry.text())
@@ -3170,10 +3160,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
             except ValueError:
                 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():
-                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.textChanged.connect(
@@ -3649,12 +3639,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.replot()
 
         # 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
         if self.app.ui.grid_snap_btn.isChecked() is False:
@@ -3916,7 +3902,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                                      _("Done."))
                                 self.select_tool(self.active_tool.name)
         except Exception as e:
-            log.warning("Error: %s" % str(e))
+            log.warning("FLatCAMGeoEditor.on_geo_click_release() --> Error: %s" % str(e))
             return
 
     def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
@@ -4246,7 +4232,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # # ## Grid snap
         if self.options["grid_snap"]:
             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:
                 snap_x_ = x
 
@@ -4254,12 +4243,18 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # and it will use the snap distance from GridX entry
             if self.app.ui.grid_gap_link_cb.isChecked():
                 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:
                     snap_y_ = y
             else:
                 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:
                     snap_y_ = 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():
                 fcgeometry.tools[self.multigeo_tool]['solid_geometry'].append(shape.geo)
             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):
         if self.paint_tooldia:
@@ -4605,27 +4600,25 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
     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
         selected = self.get_selected()
 
         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
 
         for param in [tooldia, overlap, margin]:
             if not isinstance(param, float):
                 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 = []
 
-        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):
             """
             Creates a list of non-iterable linear geometry objects.
@@ -4664,17 +4657,16 @@ class FlatCAMGeoEditor(QtCore.QObject):
                         poly_buf = Polygon(geo_obj).buffer(-margin)
 
                     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)
                     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)
-
                     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)
 
                     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):
 
-    def __init__(self, app, text=None):
+    def __init__(self, app, text=None, plain_text=None):
         super().__init__()
 
         self.app = app
@@ -39,12 +39,24 @@ class TextEditor(QtWidgets.QWidget):
         self.work_editor_layout.setContentsMargins(2, 2, 2, 2)
         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)
 
@@ -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.hide()
-        self.work_editor_layout.addWidget(self.code_editor, 0, 0, 1, 5)
 
         editor_hlay_1 = QtWidgets.QHBoxLayout()
         # 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.gcode_edited = ''
+        self.code_edited = ''
 
     def handlePrint(self):
         self.app.report_usage("handlePrint()")
@@ -168,11 +179,11 @@ class TextEditor(QtWidgets.QWidget):
             file = QtCore.QFile(path)
             if file.open(QtCore.QIODevice.ReadOnly):
                 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()
 
-    def handleSaveGCode(self, name=None, filt=None):
+    def handleSaveGCode(self, name=None, filt=None, callback=None):
         self.app.report_usage("handleSaveGCode()")
 
         if filt:
@@ -193,12 +204,12 @@ class TextEditor(QtWidgets.QWidget):
 
         try:
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export G-Code ..."),
+                caption=_("Export Code ..."),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
                 filter=_filter_
             )[0])
         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 == "":
             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.inform.emit('%s: %s' % (_("Saved to"), str(filename)))
 
+        if callback is not None:
+            callback()
+
     def handleFindGCode(self):
         self.app.report_usage("handleFindGCode()")
 
@@ -233,6 +247,7 @@ class TextEditor(QtWidgets.QWidget):
         r = self.code_editor.find(str(text_to_be_found), flags)
         if r is False:
             self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+            r = self.code_editor.find(str(text_to_be_found), flags)
 
     def handleReplaceGCode(self):
         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')
     final_save = QtCore.pyqtSignal(name='saveBeforeExit')
 
-    def __init__(self, version, beta, app):
+    def __init__(self, app):
         super(FlatCAMGUI, self).__init__()
 
         self.app = app
+        self.decimals = self.app.decimals
+
         # Divine icon pack by Ipapun @ finicons.com
 
         # ################################## ##
@@ -337,21 +339,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.menuedit.addSeparator()
         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'),
                                                                        _("&Rotate Selection\tSHIFT+(R)"))
         # Separator
@@ -373,13 +365,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'),
                                                                   _("View source\tALT+S"))
+        self.menuoptions_tools_db = self.menuoptions.addAction(QtGui.QIcon('share/database32.png'),
+                                                               _("Tools DataBase\tCTRL+D"))
         # Separator
         self.menuoptions.addSeparator()
 
         # ########################################################################
         # ########################## 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.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
                                                           _('Disable all plots\tALT+2'))
@@ -430,7 +424,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################## Tool # ######################################
         # ########################################################################
-        self.menutool = QtWidgets.QMenu(_('&Tool'))
+        self.menutool = QtWidgets.QMenu(_('Tool'))
         self.menutoolaction = self.menu.addMenu(self.menutool)
         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.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# ####################
@@ -956,6 +955,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################## PREFERENCES AREA Tab # ######################
         # ########################################################################
         self.preferences_tab = QtWidgets.QWidget()
+        self.preferences_tab.setObjectName("preferences_tab")
         self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
         self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
 
@@ -976,13 +976,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.hlay1 = QtWidgets.QHBoxLayout()
         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.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.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.setText(_("Save Preferences"))
+        self.pref_save_button.setText(_("Save"))
         self.pref_save_button.setMinimumWidth(130)
         self.pref_save_button.setToolTip(
             _("Save the current settings in the 'current_defaults' file\n"
               "which is the file storing the working default preferences."))
         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 # ##########################
         # ########################################################################
@@ -1234,6 +1241,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>CTRL+C</strong></td>
                         <td>&nbsp;%s</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+D</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>CTRL+E</strong></td>
                         <td>&nbsp;%s</td>
@@ -1254,6 +1265,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>CTRL+O</strong></td>
                         <td>&nbsp;%s</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+Q</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>CTRL+S</strong></td>
                         <td>&nbsp;%s</td>
@@ -1322,6 +1337,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>ALT+E</strong></td>
                         <td>&nbsp;%s</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+J</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>ALT+K</strong></td>
                         <td>&nbsp;%s</td>
@@ -1428,18 +1447,32 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 _("New Excellon"), _("Move Obj"), _("New Geometry"), _("Set Origin"), _("Change Units"),
                 _("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"),
-                _("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 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"),
                 _("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"),
                 _("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"),
                 _("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"),
                 _("(left to Key_1)Toogle Notebook Area (Left Side)"), _("En(Dis)able Obj Plot"),
                 _("Deselects all objects")
@@ -1971,8 +2004,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         self.setGeometry(100, 100, 1024, 650)
         self.setWindowTitle('FlatCAM %s %s - %s' %
-                            (version,
-                             ('BETA' if beta else ''),
+                            (self.app.version,
+                             ('BETA' if self.app.beta else ''),
                              platform.architecture()[0])
                             )
 
@@ -1993,23 +2026,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grb_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)
 
@@ -2020,7 +2044,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             self.restoreState(saved_gui_state)
             log.debug("FlatCAMGUI.__init__() --> UI state restored.")
 
-        settings = QSettings("Open Source", "FlatCAM")
         if settings.contains("layout"):
             layout = settings.value('layout', type=str)
             if layout == 'standard':
@@ -2064,7 +2087,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.lock_action.setText(_("Lock Toolbars"))
         self.lock_action.setCheckable(True)
 
-        settings = QSettings("Open Source", "FlatCAM")
         if settings.contains("toolbar_lock"):
             lock_val = settings.value('toolbar_lock')
             if lock_val == 'true':
@@ -2171,6 +2193,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         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.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 # ##
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
@@ -2369,28 +2396,62 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     return
 
             elif modifiers == QtCore.Qt.ControlModifier:
+                # Select All
                 if key == QtCore.Qt.Key_A:
                     self.app.on_selectall()
 
+                # Copy an FlatCAM object
                 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()
 
+                # Copy an FlatCAM object
+                if key == QtCore.Qt.Key_D:
+                    self.app.on_tools_database()
+
+                # Open Excellon file
                 if key == QtCore.Qt.Key_E:
                     self.app.on_fileopenexcellon()
 
+                # Open Gerber file
                 if key == QtCore.Qt.Key_G:
                     self.app.on_fileopengerber()
 
+                # Create New Project
                 if key == QtCore.Qt.Key_N:
                     self.app.on_file_new_click()
 
+                # Distance Tool
                 if key == QtCore.Qt.Key_M:
                     self.app.distance_tool.run()
 
+                # Open Project
                 if key == QtCore.Qt.Key_O:
                     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:
+                    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()
 
                 # Toggle Plot Area
@@ -2434,7 +2495,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
                 # Toggle Workspace
                 if key == QtCore.Qt.Key_W:
-                    self.app.on_workspace_menu()
+                    self.app.on_workspace_toggle()
                     return
 
                 # Skew on X axis
@@ -2469,9 +2530,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.dblsidedtool.run(toggle=True)
                     return
 
-                # Transformation Tool
+                # Calibrate  Tool
                 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
 
                 # Toggle Grid lines
@@ -2479,6 +2545,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_grid_lines()
                     return
 
+                # Fiducials Tool
+                if key == QtCore.Qt.Key_J:
+                    self.app.fiducial_tool.run(toggle=True)
+                    return
+
                 # Solder Paste Dispensing Tool
                 if key == QtCore.Qt.Key_K:
                     self.app.paste_tool.run(toggle=True)
@@ -2504,9 +2575,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.paint_tool.run(toggle=True)
                     return
 
-                # Paint Tool
+                # QRCode Tool
                 if key == QtCore.Qt.Key_Q:
-                    self.app.pdf_tool.run()
+                    self.app.qrcode_tool.run()
                     return
 
                 # Rules Tool
@@ -2519,9 +2590,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_view_source()
                     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
 
                 # Substract Tool
@@ -2529,6 +2600,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.sub_tool.run(toggle=True)
                     return
 
+                # Cutout Tool
+                if key == QtCore.Qt.Key_X:
+                    self.app.cutout_tool.run(toggle=True)
+                    return
+
                 # Panelize Tool
                 if key == QtCore.Qt.Key_Z:
                     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
                 # Geometry Selected tool table
                 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()
 
                 # Delete from canvas
@@ -2663,6 +2746,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
                 # Add a Tool from shortcut
                 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()
 
                 # Zoom Fit
@@ -2720,7 +2810,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
                         messagebox.exec_()
                     return
-
             elif modifiers == QtCore.Qt.ShiftModifier:
                 # Run Distance Minimum Tool
                 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':
                     self.app.geo_editor.transform_tool.on_rotate_key()
 
+                # Zoom Out
                 if key == QtCore.Qt.Key_Minus or key == '-':
                     self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
 
+                # Zoom In
                 if key == QtCore.Qt.Key_Equal or key == '=':
                     self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
                                              [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()
                     if ok:
                         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(
-                            '[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
 
                 # Zoom Fit
@@ -3444,6 +3531,54 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 # Jump to coords
                 if key == QtCore.Qt.Key_J or key == 'J':
                     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):
         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
     """
 
-    def __init__(self, movie="share/active.gif", icon='share/active_static.png', parent=None):
+    def __init__(self, app, parent=None):
         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.movie_path = movie
         self.icon_path = icon
@@ -3705,377 +3858,4 @@ class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon):
 
         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

+ 424 - 33
flatcamGUI/GUIElements.py

@@ -12,7 +12,7 @@
 # ##########################################################
 
 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.QtGui import QKeySequence, QTextCursor
 
@@ -145,7 +145,7 @@ class RadioSet(QtWidgets.QWidget):
 
 
 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)
 
         self.output_units = output_units
@@ -160,6 +160,7 @@ class LengthEntry(QtWidgets.QLineEdit):
         }
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
     def on_edit_finished(self):
         self.clearFocus()
@@ -199,12 +200,13 @@ class LengthEntry(QtWidgets.QLineEdit):
         except KeyError:
             value = raw
             return float(eval(value))
-        except:
+        except Exception:
             log.warning("Could not parse value in entry: %s" % str(raw))
             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):
         default_hint_size = super(LengthEntry, self).sizeHint()
@@ -212,10 +214,11 @@ class LengthEntry(QtWidgets.QLineEdit):
 
 
 class FloatEntry(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, decimals=None, parent=None):
         super(FloatEntry, self).__init__(parent)
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
     def on_edit_finished(self):
         self.clearFocus()
@@ -251,9 +254,10 @@ class FloatEntry(QtWidgets.QLineEdit):
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
             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:
-            self.setText("%.4f" % float(val))
+            self.setText("%.*f" % (dig_digits, float(val)))
         else:
             self.setText("")
 
@@ -263,10 +267,11 @@ class FloatEntry(QtWidgets.QLineEdit):
 
 
 class FloatEntry2(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, decimals=None, parent=None):
         super(FloatEntry2, self).__init__(parent)
         self.readyToEdit = True
         self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
 
     def on_edit_finished(self):
         self.clearFocus()
@@ -295,8 +300,9 @@ class FloatEntry2(QtWidgets.QLineEdit):
                 log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
             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):
         default_hint_size = super(FloatEntry2, self).sizeHint()
@@ -353,10 +359,20 @@ class IntEntry(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)
         self.readyToEdit = True
         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):
         self.clearFocus()
@@ -376,9 +392,10 @@ class FCEntry(QtWidgets.QLineEdit):
     def get_value(self):
         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:
-            self.setText('%.4f' % val)
+            self.setText('%.*f' % (decimal_digits, val))
         else:
             self.setText(str(val))
 
@@ -511,20 +528,37 @@ class FCSpinner(QtWidgets.QSpinBox):
 
     returnPressed = QtCore.pyqtSignal()
 
-    def __init__(self, parent=None):
+    def __init__(self, suffix=None, alignment=None, parent=None):
         super(FCSpinner, self).__init__(parent)
         self.readyToEdit = True
+
         self.editingFinished.connect(self.on_edit_finished)
         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:
-                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
 
     def keyPressEvent(self, event):
@@ -541,6 +575,7 @@ class FCSpinner(QtWidgets.QSpinBox):
 
     def on_edit_finished(self):
         self.clearFocus()
+        self.returnPressed.emit()
 
     # def mousePressEvent(self, e, parent=None):
     #     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
             self.lineEdit().deselect()
             self.readyToEdit = True
+            self.prev_readyToEdit = True
 
     def get_value(self):
         return int(self.value())
@@ -578,7 +614,7 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
 
     returnPressed = QtCore.pyqtSignal()
 
-    def __init__(self, parent=None):
+    def __init__(self, suffix=None, alignment=None, parent=None):
         super(FCDoubleSpinner, self).__init__(parent)
         self.readyToEdit = True
 
@@ -590,17 +626,35 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
         self.lineEdit().setValidator(
             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):
         self.clearFocus()
+        self.returnPressed.emit()
 
     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
 
     def keyPressEvent(self, event):
@@ -621,6 +675,7 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
             super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
             self.lineEdit().deselect()
             self.readyToEdit = True
+            self.prev_readyToEdit = True
 
     def valueFromText(self, p_str):
         text = p_str.replace(',', '.')
@@ -632,9 +687,10 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
         return ret_val
 
     def validate(self, p_str, p_int):
+        text = p_str.replace(',', '.')
         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:
             pass
         return QtGui.QValidator.Acceptable, p_str, p_int
@@ -881,6 +937,178 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
         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):
 
     def __init__(self, parent=None, callback=None):
@@ -966,6 +1194,17 @@ class FCButton(QtWidgets.QPushButton):
         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):
     def __init__(self):
         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
     def mousePressEvent(self, event):
-        if self.itemAt(event.pos()) is None:
+        if not self.itemAt(event.pos()):
             self.clearSelection()
+            self.clearFocus()
         else:
             QtWidgets.QTableWidget.mousePressEvent(self, event)
 
@@ -2161,3 +2401,154 @@ class MyCompleter(QCompleter):
 
     def getSelected(self):
         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__:
     _ = 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):
     """
@@ -29,10 +35,11 @@ class ObjectUI(QtWidgets.QWidget):
     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)
 
-        self.decimals = 4
+        self.decimals = decimals
 
         layout = QtWidgets.QVBoxLayout()
         self.setLayout(layout)
@@ -147,9 +154,9 @@ class GerberObjectUI(ObjectUI):
     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
         grid0 = QtWidgets.QGridLayout()
@@ -275,6 +282,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid1)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(1, 1)
+        grid1.setColumnStretch(2, 1)
 
         # 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"
               "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_radio, 0, 1, 1, 2)
@@ -342,7 +350,7 @@ class GerberObjectUI(ObjectUI):
         )
         tdlabel.setMinimumWidth(90)
         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.setSingleStep(0.1)
 
@@ -364,15 +372,13 @@ class GerberObjectUI(ObjectUI):
         # Pass overlap
         overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
         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)
-        self.iso_overlap_entry = FCDoubleSpinner()
+        self.iso_overlap_entry = FCDoubleSpinner(suffix='%')
         self.iso_overlap_entry.set_precision(self.decimals)
         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)
         grid1.addWidget(overlabel, 6, 0)
         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")
         )
         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_radio, 7, 1, 1, 2)
 
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
         )
@@ -400,15 +406,15 @@ class GerberObjectUI(ObjectUI):
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
                                     "This means that it will cut through\n"
                                     "the middle of the trace."))
+        grid1.addWidget(self.combine_passes_cb, 8, 0)
 
         # avoid an area from isolation
         self.except_cb = FCCheckBox(label=_('Except'))
+        grid1.addWidget(self.follow_cb, 8, 1)
+
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object bellow\n"
                                     "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)
 
         # ## Form Layout
@@ -448,8 +454,50 @@ class GerberObjectUI(ObjectUI):
 
         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"
               "isolation outside, inside or on both sides of the\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"
               "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.setToolTip(
@@ -469,48 +517,15 @@ class GerberObjectUI(ObjectUI):
               "Clicking this will create the buffered geometry\n"
               "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.except_cb,
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             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 #######################
@@ -556,6 +571,11 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.board_cutout_label, 2, 0)
         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
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label.setToolTip(
@@ -566,7 +586,7 @@ class GerberObjectUI(ObjectUI):
               "copper from a specified region.")
         )
 
-        grid2.addWidget(self.noncopper_label, 3, 0, 1, 2)
+        grid2.addWidget(self.noncopper_label, 4, 0, 1, 2)
 
         # 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.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
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
@@ -593,8 +613,13 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_rounded_cb.setMinimumWidth(90)
 
         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
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
@@ -603,7 +628,7 @@ class GerberObjectUI(ObjectUI):
               "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.setToolTip(
@@ -616,8 +641,8 @@ class GerberObjectUI(ObjectUI):
         self.bbmargin_entry.set_precision(self.decimals)
         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.setToolTip(
@@ -632,21 +657,26 @@ class GerberObjectUI(ObjectUI):
         self.generate_bb_button.setToolTip(
             _("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):
     """
     User interface for Excellon objects.
     """
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         ObjectUI.__init__(self, title=_('Excellon Object'),
                           icon_file='share/drill32.png',
-                          parent=parent)
+                          parent=parent,
+                          decimals=decimals)
 
-        self.decimals = 4
+        self.decimals = decimals
 
         # ### Plot options ####
         hlay_plot = QtWidgets.QHBoxLayout()
@@ -754,7 +784,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = FCDoubleSpinner()
         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)
 
         grid1.addWidget(self.cutz_entry, 0, 1)
@@ -768,7 +803,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = FCDoubleSpinner()
         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)
 
         grid1.addWidget(self.travelz_entry, 1, 1)
@@ -790,7 +830,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(toolchzlabel, 3, 0)
         self.toolchangez_entry = FCDoubleSpinner()
         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)
 
         grid1.addWidget(self.toolchangez_entry, 3, 1)
@@ -815,7 +860,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.eendz_label, 5, 0)
         self.eendz_entry = FCDoubleSpinner()
         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)
 
         grid1.addWidget(self.eendz_entry, 5, 1)
@@ -885,10 +935,10 @@ class ExcellonObjectUI(ObjectUI):
 
         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(
-            _("The postprocessor JSON file that dictates\n"
+            _("The preprocessor JSON file that dictates\n"
               "Gcode output.")
         )
         self.pp_excellon_name_cb = FCComboBox()
@@ -934,12 +984,11 @@ class ExcellonObjectUI(ObjectUI):
         grid2.setColumnStretch(0, 0)
         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
         gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
@@ -967,17 +1016,12 @@ class ExcellonObjectUI(ObjectUI):
         # ### Milling Holes Drills ####
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         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)
 
-        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.setToolTip(
             _("Diameter of the cutting tool.")
@@ -993,9 +1037,9 @@ class ExcellonObjectUI(ObjectUI):
               "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.setToolTip(
@@ -1014,9 +1058,9 @@ class ExcellonObjectUI(ObjectUI):
               "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):
         if state is True:
@@ -1030,10 +1074,10 @@ class GeometryObjectUI(ObjectUI):
     User interface for Geometry objects.
     """
 
-    def __init__(self, parent=None):
+    def __init__(self, decimals, parent=None):
         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
         self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
@@ -1152,6 +1196,8 @@ class GeometryObjectUI(ObjectUI):
         # Tool Offset
         self.grid1 = QtWidgets.QGridLayout()
         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.setToolTip(
@@ -1162,70 +1208,57 @@ class GeometryObjectUI(ObjectUI):
                 "cut and negative for 'inside' cut."
             )
         )
-        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
         self.tool_offset_entry = FCDoubleSpinner()
         self.tool_offset_entry.set_precision(self.decimals)
         self.tool_offset_entry.setRange(-9999.9999, 9999.9999)
         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.setToolTip(
-            _(
-                "Diameter for the new tool"
-            )
+            _("Diameter for the new tool")
         )
         self.addtool_entry = FCDoubleSpinner()
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.setRange(0.00001, 9999.9999)
         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.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.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.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.geo_tools_box.addWidget(self.empty_label)
@@ -1233,8 +1266,15 @@ class GeometryObjectUI(ObjectUI):
         # ##################
         # 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 ## ##
-        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(
             _(
                 "The data used for creating GCode.\n"
@@ -1295,7 +1335,12 @@ class GeometryObjectUI(ObjectUI):
         )
         self.cutz_entry = FCDoubleSpinner()
         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.grid3.addWidget(cutzlabel, 3, 0)
@@ -1335,7 +1380,12 @@ class GeometryObjectUI(ObjectUI):
         )
         self.travelz_entry = FCDoubleSpinner()
         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.grid3.addWidget(travelzlabel, 5, 0)
@@ -1358,7 +1408,12 @@ class GeometryObjectUI(ObjectUI):
         )
         self.toolchangez_entry = FCDoubleSpinner()
         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.grid3.addWidget(self.toolchangeg_cb, 6, 0, 1, 2)
@@ -1385,14 +1440,19 @@ class GeometryObjectUI(ObjectUI):
         )
         self.gendz_entry = FCDoubleSpinner()
         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.grid3.addWidget(self.endzlabel, 9, 0)
         self.grid3.addWidget(self.gendz_entry, 9, 1)
 
         # Feedrate X-Y
-        frlabel = QtWidgets.QLabel('%s:' % _('Feed Rate X-Y'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate X-Y'))
         frlabel.setToolTip(
             _("Cutting speed in the XY\n"
               "plane in units per minute")
@@ -1406,7 +1466,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
 
         # Feedrate Z (Plunge)
-        frzlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Z'))
+        frzlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
         frzlabel.setToolTip(
             _("Cutting speed in the XY\n"
               "plane in units per minute.\n"
@@ -1421,7 +1481,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
 
         # Feedrate rapids
-        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Rapids'))
+        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
         self.fr_rapidlabel.setToolTip(
             _("Cutting speed in the XY plane\n"
               "(in units per minute).\n"
@@ -1455,7 +1515,7 @@ class GeometryObjectUI(ObjectUI):
         spdlabel.setToolTip(
             _(
                 "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."
             )
         )
@@ -1485,10 +1545,10 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.dwell_cb, 15, 0)
         self.grid3.addWidget(self.dwelltime_entry, 15, 1)
 
-        # postprocessor selection
+        # preprocessor selection
         pp_label = QtWidgets.QLabel('%s:' % _("PostProcessor"))
         pp_label.setToolTip(
-            _("The Postprocessor file that dictates\n"
+            _("The Preprocessor file that dictates\n"
               "the Machine Code (like GCode, RML, HPGL) output.")
         )
         self.pp_geometry_name_cb = FCComboBox()
@@ -1530,16 +1590,30 @@ class GeometryObjectUI(ObjectUI):
         self.feedrate_probe_label.hide()
         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(
             _(
                 "Add at least one tool in the tool-table.\n"
                 "Click the header to select all, or Ctrl + LMB\n"
                 "for custom selection of tools."
             ))
-        self.grid3.addWidget(warning_lbl, 19, 0, 1, 2)
+        self.grid3.addWidget(warning_lbl, 22, 0, 1, 2)
 
         # Button
-        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate'))
+        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate CNCJob object'))
         self.generate_cnc_button.setToolTip(
             _("Generate the CNC Job object.")
         )
@@ -1572,14 +1646,15 @@ class CNCObjectUI(ObjectUI):
     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
         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()):
             self.common_grid.itemAt(i).widget().hide()
@@ -1744,6 +1819,10 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(prependlabel)
 
         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)
 
         # Append text to GCode
@@ -1756,6 +1835,11 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(appendlabel)
 
         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.cnc_frame = QtWidgets.QFrame()
@@ -1774,7 +1858,7 @@ class CNCObjectUI(ObjectUI):
                 "This will constitute a Custom Toolchange GCode,\n"
                 "or a Toolchange Macro.\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"
                 "having as template the 'Toolchange Custom' posprocessor file."
             )
@@ -1782,6 +1866,17 @@ class CNCObjectUI(ObjectUI):
         self.cnc_box.addWidget(self.toolchangelabel)
 
         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)
 
         cnclay = QtWidgets.QHBoxLayout()
@@ -1861,7 +1956,7 @@ class ScriptObjectUI(ObjectUI):
     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
         be placed in ``self.custom_box`` to preserve the layout.
@@ -1870,7 +1965,10 @@ class ScriptObjectUI(ObjectUI):
         ObjectUI.__init__(self, title=_('Script Object'),
                           icon_file='share/script_new24.png',
                           parent=parent,
-                          common=False)
+                          common=False,
+                          decimals=decimals)
+
+        self.decimals = decimals
 
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
@@ -1913,7 +2011,7 @@ class DocumentObjectUI(ObjectUI):
     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
         be placed in ``self.custom_box`` to preserve the layout.
@@ -1922,7 +2020,10 @@ class DocumentObjectUI(ObjectUI):
         ObjectUI.__init__(self, title=_('Document Object'),
                           icon_file='share/notes16_1.png',
                           parent=parent,
-                          common=False)
+                          common=False,
+                          decimals=decimals)
+
+        self.decimals = decimals
 
         # ## Object name
         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,
         # 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>
         self.create_native()
@@ -67,16 +121,16 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.container.addWidget(self.native)
 
         # ## 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)
 
-        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)
 
         # 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
-
-        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.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)
 
-    # 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:
-            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):
         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
 
-    # 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):
         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
 
     def graph_event_connect(self, event_name, callback):
@@ -310,12 +334,32 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
             except TypeError:
                 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
-        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
 

+ 113 - 4
flatcamGUI/PlotCanvasLegacy.py

@@ -30,6 +30,7 @@ from matplotlib import use as mpl_use
 mpl_use("Qt5Agg")
 from matplotlib.figure import Figure
 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.lines import Line2D
 # from matplotlib.widgets import Cursor
 
 fcTranslate.apply_language('strings')
@@ -152,6 +153,64 @@ class PlotCanvasLegacy(QtCore.QObject):
             theme_color = '#000000'
             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
         self.x_margin = 15  # 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.set_aspect(1)
         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='y', color=tick_color, labelcolor=tick_color)
@@ -240,6 +299,44 @@ class PlotCanvasLegacy(QtCore.QObject):
         # signal if there is a doubleclick
         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):
         """
         Attach an event handler to the canvas through the Matplotlib interface.
@@ -272,8 +369,6 @@ class PlotCanvasLegacy(QtCore.QObject):
         :return: None
         """
 
-        # self.double_click.disconnect(cid)
-
         self.canvas.mpl_disconnect(cid)
 
     def on_new_screen(self):
@@ -911,6 +1006,14 @@ class ShapeCollectionLegacy:
 
         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):
         """
         Clear the canvas of the shapes.
@@ -936,6 +1039,7 @@ class ShapeCollectionLegacy:
 
         :return: None
         """
+
         path_num = 0
         local_shapes = deepcopy(self._shapes)
 
@@ -945,6 +1049,10 @@ class ShapeCollectionLegacy:
             obj_type = 'utility'
 
         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:
                 if obj_type == 'excellon':
                     # Plot excellon (All polygons?)
@@ -1040,6 +1148,7 @@ class ShapeCollectionLegacy:
                                                  edgecolor=local_shapes[element]['color'],
                                                  alpha=local_shapes[element]['alpha'],
                                                  zorder=2)
+
                             self.axes.add_patch(patch)
                         except Exception as 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,
                                          maj + majstep - minstep,
                                          minor_num))
+
             major_frac = (major - offset) / scale
-            minor_frac = (np.array(minor) - offset) / scale
             major_frac = major_frac[::-1] if flip else major_frac
             use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
             major_frac = major_frac[use_mask]
             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':
             return NotImplementedError
         elif self.axis.scale_type == 'power':
             return NotImplementedError
-        return major_frac, minor_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)
     return geo
 
+
 def dxfline2shapely(line):
 
     try:
@@ -39,6 +40,7 @@ def dxfline2shapely(line):
 
     return geo
 
+
 def dxfcircle2shapely(circle, n_points=100):
 
     ocs = circle.ocs()
@@ -241,7 +243,7 @@ def dxfsolid2shapely(solid):
     try:
         corner_list.append(solid[iterator])
         iterator += 1
-    except:
+    except Exception:
         return Polygon(corner_list)
 
 
@@ -265,7 +267,7 @@ def dxftrace2shapely(trace):
     try:
         corner_list.append(trace[iterator])
         iterator += 1
-    except:
+    except Exception:
         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
 import FlatCAMApp
@@ -9,6 +16,7 @@ import numpy as np
 import re
 import logging
 import traceback
+from copy import deepcopy
 
 import FlatCAMTranslation as fcTranslate
 
@@ -77,6 +85,7 @@ class Excellon(Geometry):
         :return: Excellon object.
         :rtype: Excellon
         """
+        self.decimals = self.app.decimals
 
         if geo_steps_per_circle is None:
             geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
@@ -88,7 +97,6 @@ class Excellon(Geometry):
         self.tools = {}
         # list to store the drills, see above for description
         self.drills = []
-
         # self.slots (list) to store the slots; each is a dictionary
         self.slots = []
 
@@ -105,7 +113,7 @@ class Excellon(Geometry):
         self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
 
         # ## IN|MM -> Units are inherited from Geometry
-        # self.units = units
+        self.units = self.app.defaults['units']
 
         # Trailing "T" or leading "L" (default)
         # self.zeros = "T"
@@ -250,7 +258,7 @@ class Excellon(Geometry):
 
         try:
             self.parse_lines(estr)
-        except:
+        except Exception:
             return "fail"
 
     def parse_lines(self, elines):
@@ -303,8 +311,7 @@ class Excellon(Geometry):
                 # and we need to exit from here
                 if self.detect_gcode_re.search(eline):
                     log.warning("This is GCODE mark: %s" % eline)
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_('This is GCODE mark'), eline))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_('This is GCODE mark'), eline))
                     return
 
                 # Header Begin (M48) #
@@ -339,11 +346,11 @@ class Excellon(Geometry):
                             if line_units == 'MILS':
                                 spec = {"C": (float(match.group(2)) / 1000)}
                                 self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                                log.debug("Tool definition: %s %s" % (name_tool, spec))
                             else:
                                 spec = {"C": float(match.group(2))}
                                 self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                                log.debug("Tool definition: %s %s" % (name_tool, spec))
                             spec['solid_geometry'] = []
                             continue
                     # search for Altium Excellon Format / Sprint Layout who is included as a comment
@@ -385,17 +392,18 @@ class Excellon(Geometry):
                 # object's units.
                 match = self.meas_re.match(eline)
                 if match:
-                    # self.units = {"1": "MM", "2": "IN"}[match.group(1)]
+                    self.units = {"1": "MM", "2": "IN"}[match.group(1)]
 
                     # Modified for issue #80
-                    self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
-                    log.debug("  Units: %s" % self.units)
+                    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':
-                        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:
-                        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
 
                 # ### Body ####
@@ -412,7 +420,7 @@ class Excellon(Geometry):
                                 name = str(int(match.group(1)))
                                 try:
                                     diam = float(match.group(2))
-                                except:
+                                except Exception:
                                     # it's possible that tool definition has only tool number and no diameter info
                                     # (those could be in another file like PCB Wizard do)
                                     # then match.group(2) = None and float(None) will create the exception
@@ -476,7 +484,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_start_x
                             except TypeError:
                                 slot_start_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -484,7 +492,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_start_y
                             except TypeError:
                                 slot_start_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -492,7 +500,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_stop_x
                             except TypeError:
                                 slot_stop_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -500,7 +508,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_stop_y
                             except TypeError:
                                 slot_stop_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
 
                             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
                             except TypeError:
                                 slot_start_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -554,7 +562,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_start_y
                             except TypeError:
                                 slot_start_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -562,7 +570,7 @@ class Excellon(Geometry):
                                 slot_current_x = slot_stop_x
                             except TypeError:
                                 slot_stop_x = slot_current_x
-                            except:
+                            except Exception:
                                 return
 
                             try:
@@ -570,7 +578,7 @@ class Excellon(Geometry):
                                 slot_current_y = slot_stop_y
                             except TypeError:
                                 slot_stop_y = slot_current_y
-                            except:
+                            except Exception:
                                 return
 
                             if (slot_start_x is None or slot_start_y is None or
@@ -619,7 +627,7 @@ class Excellon(Geometry):
                         except TypeError:
                             x = current_x
                             repeating_x = 0
-                        except:
+                        except Exception:
                             return
 
                         try:
@@ -629,7 +637,7 @@ class Excellon(Geometry):
                         except TypeError:
                             y = current_y
                             repeating_y = 0
-                        except:
+                        except Exception:
                             return
 
                         if x is None or y is None:
@@ -776,13 +784,13 @@ class Excellon(Geometry):
                         name = str(int(match.group(1)))
                         spec = {"C": float(match.group(2)), 'solid_geometry': []}
                         self.tools[name] = spec
-                        log.debug("  Tool definition: %s %s" % (name, spec))
+                        log.debug("Tool definition: %s %s" % (name, spec))
                         continue
 
                     # ## Units and number format # ##
                     match = self.units_re.match(eline)
                     if match:
-                        self.units_found = match.group(1)
+                        self.units = self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
                         self.excellon_format = match.group(3)
                         if self.excellon_format:
@@ -796,51 +804,49 @@ class Excellon(Geometry):
                                 self.excellon_format_lower_in = lower
 
                         # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                        # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                        log.warning("Units: %s" % self.units)
+                        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':
-                            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:
-                            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
 
                     # Search for units type again it might be alone on the line
                     if "INCH" in eline:
-                        line_units = "INCH"
+                        line_units = "IN"
                         # 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
                     elif "METRIC" in eline:
-                        line_units = "METRIC"
+                        line_units = "MM"
                         # 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
 
                     # Search for zeros type again because it might be alone on the line
                     match = re.search(r'[LT]Z', eline)
                     if match:
                         self.zeros = match.group()
-                        log.warning("Type of zeros found: %s" % self.zeros)
+                        log.warning("Type of ZEROS found: %s" % self.zeros)
                         continue
 
                 # ## Units and number format outside header# ##
                 match = self.units_re.match(eline)
                 if match:
-                    self.units_found = match.group(1)
+                    self.units = self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
                     self.zeros = match.group(2)  # "T" or "L". Might be empty
                     self.excellon_format = match.group(3)
                     if self.excellon_format:
@@ -854,18 +860,17 @@ class Excellon(Geometry):
                             self.excellon_format_lower_in = lower
 
                     # Modified for issue #80
-                    self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                    # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                    log.warning("Units: %s" % self.units)
+                    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':
-                        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:
-                        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
 
                 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
             # is finished since the tools definitions are spread in the Excellon body. We use as units the value
             # from self.defaults['excellon_units']
+
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
-        except Exception as e:
+        except Exception:
             log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
             msg = '[ERROR_NOTCL] %s' % \
                   _("An internal error has ocurred. See shell.\n")
@@ -948,6 +954,8 @@ class Excellon(Geometry):
 
         :return: None
         """
+
+        log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry()")
         self.solid_geometry = []
         try:
             # clear the solid_geometry in self.tools
@@ -964,7 +972,7 @@ class Excellon(Geometry):
                                          _("Excellon.create_geometry() -> a drill location was skipped "
                                            "due of not having a tool associated.\n"
                                            "Check the resulting GCode."))
-                    log.debug("Excellon.create_geometry() -> a drill location was skipped "
+                    log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry() -> a drill location was skipped "
                               "due of not having a tool associated")
                     continue
                 tooldia = self.tools[drill['tool']]['C']
@@ -983,37 +991,10 @@ class Excellon(Geometry):
                 self.tools[slot['tool']]['solid_geometry'].append(poly)
 
         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"
 
-        # drill_geometry = {}
-        # slot_geometry = {}
-        #
-        # def insertIntoDataStruct(dia, drill_geo, aDict):
-        #     if not dia in aDict:
-        #         aDict[dia] = [drill_geo]
-        #     else:
-        #         aDict[dia].append(drill_geo)
-        #
-        # for tool in self.tools:
-        #     tooldia = self.tools[tool]['C']
-        #     for drill in self.drills:
-        #         if drill['tool'] == tool:
-        #             poly = drill['point'].buffer(tooldia / 2.0)
-        #             insertIntoDataStruct(tooldia, poly, drill_geometry)
-        #
-        # for tool in self.tools:
-        #     slot_tooldia = self.tools[tool]['C']
-        #     for slot in self.slots:
-        #         if slot['tool'] == tool:
-        #             start = slot['start']
-        #             stop = slot['stop']
-        #             lines_string = LineString([start, stop])
-        #             poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
-        #             insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
-        #
-        # self.solid_geometry = [drill_geometry, slot_geometry]
-
     def bounds(self):
         """
         Returns coordinates of rectangular bounds
@@ -1022,9 +1003,10 @@ class Excellon(Geometry):
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
 
-        log.debug("camlib.Excellon.bounds()")
-        if self.solid_geometry is None:
-            log.debug("solid_geometry is None")
+        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
 
         def bounds_rec(obj):
@@ -1065,7 +1047,7 @@ class Excellon(Geometry):
             maxx_list.append(maxx)
             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):
         """
@@ -1082,16 +1064,29 @@ class Excellon(Geometry):
         :type str: IN or MM
         :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
         for tname in self.tools:
             self.tools[tname]["C"] *= factor
 
         self.create_geometry()
-
         return factor
 
     def scale(self, xfactor, yfactor=None, point=None):
@@ -1106,7 +1101,7 @@ class Excellon(Geometry):
         :return: None
         :rtype: NOne
         """
-        log.debug("camlib.Excellon.scale()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.scale()")
 
         if yfactor is None:
             yfactor = xfactor
@@ -1117,6 +1112,9 @@ class Excellon(Geometry):
         else:
             px, py = point
 
+        if xfactor == 0 and yfactor == 0:
+            return
+
         def scale_geom(obj):
             if type(obj) is list:
                 new_obj = []
@@ -1169,10 +1167,13 @@ class Excellon(Geometry):
         :type vect: tuple
         :return: None
         """
-        log.debug("camlib.Excellon.offset()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.offset()")
 
         dx, dy = vect
 
+        if dx == 0 and dy == 0:
+            return
+
         def offset_geom(obj):
             if type(obj) is list:
                 new_obj = []
@@ -1227,7 +1228,7 @@ class Excellon(Geometry):
         :type point: list
         :return: None
         """
-        log.debug("camlib.Excellon.mirror()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.mirror()")
 
         px, py = point
         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:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
-        log.debug("camlib.Excellon.skew()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.skew()")
 
         if angle_x is None:
             angle_x = 0.0
@@ -1302,6 +1303,9 @@ class Excellon(Geometry):
         if angle_y is None:
             angle_y = 0.0
 
+        if angle_x == 0 and angle_y == 0:
+            return
+
         def skew_geom(obj):
             if type(obj) is list:
                 new_obj = []
@@ -1378,7 +1382,10 @@ class Excellon(Geometry):
         :param point: tuple of coordinates (x, y)
         :return:
         """
-        log.debug("camlib.Excellon.rotate()")
+        log.debug("flatcamParsers.ParseExcellon.Excellon.rotate()")
+
+        if angle == 0:
+            return
 
         def rotate_geom(obj, origin=None):
             if type(obj) is list:

+ 111 - 18
flatcamParsers/ParseGerber.py

@@ -9,13 +9,15 @@ import traceback
 from copy import deepcopy
 import sys
 
-from shapely.ops import cascaded_union
+from shapely.ops import cascaded_union, unary_union
 from shapely.geometry import Polygon, MultiPolygon, LineString, Point
 import shapely.affinity as affinity
 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 builtins
 
@@ -81,6 +83,7 @@ class Gerber(Geometry):
 
         # How to approximate a circle with lines.
         self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
+        self.decimals = self.app.decimals
 
         # Initialize parent
         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
             paramList = apParameters.split('X')
-        except:
+        except Exception:
             paramList = None
 
         if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
@@ -784,7 +787,7 @@ class Gerber(Geometry):
                         self.apertures['0'] = {}
                         self.apertures['0']['type'] = 'REG'
                         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
                     # 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:
                     #     try:
                     #         current_operation_code = int(match.group(4))
-                    #     except:
+                    #     except Exception:
                     #         pass  # A line with just * will match too.
                     #     continue
                     # NOTE: Letting it continue allows it to react to the
@@ -1082,7 +1085,7 @@ class Gerber(Geometry):
                                             geo_dict['clear'] = geo_s
                                         else:
                                             geo_dict['solid'] = geo_s
-                                except:
+                                except Exception:
                                     if self.app.defaults['gerber_simplification']:
                                         poly_buffer.append(geo_s.simplify(s_tol))
                                     else:
@@ -1434,7 +1437,7 @@ class Gerber(Geometry):
                 #     for poly in new_poly:
                 #         try:
                 #             self.solid_geometry = self.solid_geometry.union(poly)
-                #         except:
+                #         except Exception:
                 #             pass
             else:
                 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
         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.
         :rtype: float
         """
@@ -1635,6 +1638,87 @@ class Gerber(Geometry):
         self.scale(factor, 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):
         """
         Scales the objects' geometry on the XY plane by a given factor.
@@ -1661,7 +1745,7 @@ class Gerber(Geometry):
 
         try:
             xfactor = float(xfactor)
-        except:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Scale factor has to be a number: integer or float."))
             return
@@ -1671,11 +1755,14 @@ class Gerber(Geometry):
         else:
             try:
                 yfactor = float(yfactor)
-            except:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("Scale factor has to be a number: integer or float."))
                 return
 
+        if xfactor == 0 and yfactor == 0:
+            return
+
         if point is None:
             px = 0
             py = 0
@@ -1685,8 +1772,7 @@ class Gerber(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            for __ in self.solid_geometry:
-                self.geo_len += 1
+            self.geo_len = len(self.solid_geometry)
         except TypeError:
             self.geo_len = 1
 
@@ -1751,8 +1837,7 @@ class Gerber(Geometry):
             log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
             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 = ''
 
         # ## solid_geometry ???
@@ -1792,6 +1877,9 @@ class Gerber(Geometry):
                                    "Probable you entered only one value in the Offset field."))
             return
 
+        if dx == 0 and dy == 0:
+            return
+
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
@@ -1944,11 +2032,13 @@ class Gerber(Geometry):
 
         px, py = point
 
+        if angle_x == 0 and angle_y == 0:
+            return
+
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            for __ in self.solid_geometry:
-                self.geo_len += 1
+            self.geo_len = len(self.solid_geometry)
         except TypeError:
             self.geo_len = 1
 
@@ -2005,6 +2095,9 @@ class Gerber(Geometry):
 
         px, py = point
 
+        if angle == 0:
+            return
+
         # variables to display the percentage of work done
         self.geo_len = 0
         try:

+ 13 - 6
flatcamParsers/ParseSVG.py

@@ -22,7 +22,7 @@
 # import xml.etree.ElementTree as ET
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
 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
 import numpy as np
 
@@ -71,7 +71,6 @@ def path2shapely(path, object_type, res=1.0):
     rings = []
 
     for component in path:
-
         # Line
         if isinstance(component, Line):
             start = component.start
@@ -123,17 +122,25 @@ def path2shapely(path, object_type, res=1.0):
 
     if points:
         rings.append(points)
+
+    rings = MultiLineString(rings)
     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
             if Point(rings[0][0]).almost_equals(Point(rings[0][-1])) and len(rings[0]) > 2:
                 geo_element = Polygon(rings[0])
             else:
                 geo_element = LineString(rings[0])
         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)
-
     return geometry
 
 
@@ -298,7 +305,7 @@ def getsvggeo(node, object_type, root=None):
         root = node
 
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
-    geo = []
+    geo = list()
 
     # Recurse
     if len(node) > 0:

+ 18 - 5
flatcamTools/ToolCalculators.py

@@ -30,7 +30,7 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
 
         self.app = app
-        self.decimals = 6
+        self.decimals = self.app.decimals
 
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -94,6 +94,8 @@ class ToolCalculator(FlatCAMTool):
         self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
         self.tipDia_entry = FCDoubleSpinner()
         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_label.setToolTip(
@@ -102,6 +104,8 @@ class ToolCalculator(FlatCAMTool):
         )
         self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
         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_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_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.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -163,6 +167,7 @@ class ToolCalculator(FlatCAMTool):
         self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
         self.pcblength_entry = FCDoubleSpinner()
         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.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
@@ -170,6 +175,7 @@ class ToolCalculator(FlatCAMTool):
         self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
         self.pcbwidth_entry = FCDoubleSpinner()
         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.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_entry = FCDoubleSpinner()
         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_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_entry = FCDoubleSpinner()
         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_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.cvalue_entry = FCDoubleSpinner()
         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.cvaluelabel.setToolTip(_('This is the current intensity value\n'
@@ -204,6 +216,8 @@ class ToolCalculator(FlatCAMTool):
         self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
         self.time_entry = FCDoubleSpinner()
         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.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)
 
     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
         self.mm_entry.set_value('%.*f' % (self.decimals, 0))
@@ -311,8 +325,7 @@ class ToolCalculator(FlatCAMTool):
 
         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 = -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 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 shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing
@@ -30,6 +30,12 @@ if '_' not in builtins.__dict__:
 
 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):
 
@@ -40,7 +46,7 @@ class CutOut(FlatCAMTool):
 
         self.app = app
         self.canvas = app.plotcanvas
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -54,8 +60,10 @@ class CutOut(FlatCAMTool):
         self.layout.addWidget(title_label)
 
         # 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
         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(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(
             _("Specify the type of object to be cutout.\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.")
         )
         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
         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.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
-        self.kindlabel = QtWidgets.QLabel('%s:' % _('Obj kind'))
+        self.kindlabel = QtWidgets.QLabel('%s:' % _('Object kind'))
         self.kindlabel.setToolTip(
             _("Choice of what kind the object we want to cutout is.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
@@ -103,43 +112,95 @@ class CutOut(FlatCAMTool):
             {"label": _("Single"), "value": "single"},
             {"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
         self.dia = FCDoubleSpinner()
         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(
            _("Diameter of the tool used to cutout\n"
              "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
         self.margin = FCDoubleSpinner()
         self.margin.set_precision(self.decimals)
 
-        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
              "will make the cutout of the PCB further from\n"
              "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
         self.gapsize = FCDoubleSpinner()
         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(
            _("The size of the bridge gaps in the cutout\n"
              "used to keep the board connected to\n"
              "the surrounding material (the one \n"
              "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:
         # lr    - left + right
@@ -150,13 +211,18 @@ class CutOut(FlatCAMTool):
         # 8     - 2*left + 2*right +2*top + 2*bottom
 
         # 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"
               "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
         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)
 
         # 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(
             _("Cutout the selected object.\n"
               "The cutout shape can be of any shape.\n"
               "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(
             _("Cutout the selected object.\n"
               "The resulting cutout shape is\n"
               "always a rectangle shape and it will be\n"
               "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
         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.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(
             _("Geometry object used to create the manual cutout.")
         )
         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(
             _("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_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(
             _("Use the left mouse button (LMB) click\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 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()
 
+        # ## 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_dia = 0.0
 
@@ -339,6 +399,7 @@ class CutOut(FlatCAMTool):
         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_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):
         obj_type = self.type_obj_combo.currentIndex()
@@ -373,7 +434,7 @@ class CutOut(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Cutout Tool"))
 
     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):
         self.reset_fields()
@@ -381,6 +442,10 @@ class CutOut(FlatCAMTool):
         self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
         self.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"])
         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.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         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['ymax'] = ymax
             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"
         self.app.new_object('geometry', outname, geo_init)
@@ -702,6 +770,9 @@ class CutOut(FlatCAMTool):
 
             geo_obj.solid_geometry = deepcopy(solid_geo)
             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"
         self.app.new_object('geometry', outname, geo_init)
@@ -842,6 +913,9 @@ class CutOut(FlatCAMTool):
                         solid_geo.append(poly.exterior)
                     geo_obj.solid_geometry = deepcopy(solid_geo)
             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"
         self.app.new_object('geometry', outname, geo_init)

+ 63 - 35
flatcamTools/ToolDblSided.py

@@ -26,7 +26,7 @@ class DblSidedTool(FlatCAMTool):
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -45,6 +45,8 @@ class DblSidedTool(FlatCAMTool):
         # ## Grid Layout
         grid_lay = QtWidgets.QGridLayout()
         self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
 
         # ## Gerber Object to mirror
         self.gerber_object_combo = QtWidgets.QComboBox()
@@ -53,9 +55,7 @@ class DblSidedTool(FlatCAMTool):
         self.gerber_object_combo.setCurrentIndex(1)
 
         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.setToolTip(
@@ -63,6 +63,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
+        self.mirror_gerber_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_gerber_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -77,9 +83,7 @@ class DblSidedTool(FlatCAMTool):
         self.exc_object_combo.setCurrentIndex(1)
 
         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.setToolTip(
@@ -87,6 +91,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
+        self.mirror_exc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_exc_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -111,6 +121,12 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
+        self.mirror_geo_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.mirror_geo_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
@@ -126,9 +142,8 @@ class DblSidedTool(FlatCAMTool):
         self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
                                      {'label': 'Y', 'value': 'Y'}])
         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)
         self.empty_lb1 = QtWidgets.QLabel("")
         grid_lay1.addWidget(self.empty_lb1, 6, 0)
@@ -154,6 +169,8 @@ class DblSidedTool(FlatCAMTool):
         # ## Grid Layout
         grid_lay2 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid_lay2)
+        grid_lay2.setColumnStretch(0, 1)
+        grid_lay2.setColumnStretch(1, 0)
 
         # ## Point/Box
         self.point_box_container = QtWidgets.QVBoxLayout()
@@ -172,6 +189,12 @@ class DblSidedTool(FlatCAMTool):
               "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.")
         )
+        self.add_point_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.add_point_button.setMinimumWidth(60)
 
         grid_lay2.addWidget(self.pb_label, 10, 0)
@@ -187,9 +210,9 @@ class DblSidedTool(FlatCAMTool):
         self.box_combo.setCurrentIndex(1)
 
         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)
@@ -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"
               "- 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)
 
         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.drill_dia, 1, 1)
 
-        hlay2 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay2)
-
         # ## Buttons
         self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
         self.create_alignment_hole_button.setToolTip(
@@ -262,16 +288,28 @@ class DblSidedTool(FlatCAMTool):
               "specified alignment holes and their mirror\n"
               "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(
-            _("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
         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]
 
-        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 '':
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("No value or wrong format in Drill Dia entry. Add it and retry."))
             return
+
         tools = {"1": {"C": dia}}
 
         # 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.GUIElements import FCEntry
 
-import copy
+from copy import copy
 import math
 import logging
 import gettext
@@ -33,8 +33,10 @@ class Distance(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
 
         self.app = app
+        self.decimals = self.app.decimals
+
         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_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
@@ -135,8 +137,6 @@ class Distance(FlatCAMTool):
         self.mm = None
         self.mr = None
 
-        self.decimals = 4
-
         # VisPy visuals
         if self.app.is_legacy is False:
             self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
@@ -182,7 +182,7 @@ class Distance(FlatCAMTool):
 
         # Switch notebook to tool page
         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"
 
@@ -194,6 +194,12 @@ class Distance(FlatCAMTool):
         self.distance_y_entry.set_value('0.0')
         self.angle_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")
 
     def activate_measure_tool(self):
@@ -204,7 +210,7 @@ class Distance(FlatCAMTool):
         self.original_call_source = copy(self.app.call_source)
 
         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
         # 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.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_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
@@ -137,7 +138,6 @@ class DistanceMin(FlatCAMTool):
 
         self.layout.addStretch()
 
-        self.decimals = 4
         self.h_point = (0, 0)
 
         self.measure_btn.clicked.connect(self.activate_measure_tool)
@@ -175,7 +175,7 @@ class DistanceMin(FlatCAMTool):
         # Switch notebook to tool page
         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
         self.start_entry.set_value('(0, 0)')
@@ -195,7 +195,7 @@ class DistanceMin(FlatCAMTool):
         # ENABLE the Measuring TOOL
         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':
             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 flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
-    OptionalHideInputSection, OptionalInputSection
+    OptionalHideInputSection, OptionalInputSection, FCComboBox
 
 from copy import deepcopy
 import logging
 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 FlatCAMTranslation as fcTranslate
 import builtins
@@ -33,7 +44,7 @@ class Film(FlatCAMTool):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
 
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         # Title
         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.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
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb.setToolTip(
@@ -179,7 +196,7 @@ class Film(FlatCAMTool):
             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_entry = FCDoubleSpinner()
@@ -187,8 +204,8 @@ class Film(FlatCAMTool):
         self.film_skewx_entry.set_precision(self.decimals)
         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_entry = FCDoubleSpinner()
@@ -196,8 +213,8 @@ class Film(FlatCAMTool):
         self.film_skewy_entry.set_precision(self.decimals)
         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.setToolTip(
@@ -211,12 +228,18 @@ class Film(FlatCAMTool):
                                             orientation='vertical',
                                             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.film_skewy_label,  self.film_skewy_entry,
                                                                  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
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb.setToolTip(
@@ -227,7 +250,7 @@ class Film(FlatCAMTool):
             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'},
                                           {'label': _('X'), 'value': 'x'},
@@ -236,13 +259,20 @@ class Film(FlatCAMTool):
                                          stretch=False)
         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.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
         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"
               "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
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
@@ -274,8 +304,8 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "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
         self.boundary_entry = FCDoubleSpinner()
@@ -294,8 +324,8 @@ class Film(FlatCAMTool):
               "white color like the rest and which may confound with the\n"
               "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_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"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "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
         self.punch_frame = QtWidgets.QFrame()
@@ -359,21 +389,146 @@ class Film(FlatCAMTool):
         self.punch_size_label.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.setToolTip(
             _("Create a Film for the selected object, within\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()
 
+        # ## 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
         self.film_object_button.clicked.connect(self.on_film_creation)
         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.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):
         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_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.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):
         type_of_film = val
@@ -463,6 +623,18 @@ class Film(FlatCAMTool):
             self.boundary_entry.hide()
             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):
         if val == 'pad' and self.punch_cb.get_value():
             self.punch_size_label.show()
@@ -485,21 +657,21 @@ class Film(FlatCAMTool):
 
         try:
             name = self.tf_object_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
             return
 
         try:
             boxname = self.tf_box_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
 
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
-
         source = self.source_punch.get_value()
+        file_type = self.file_type_radio.get_value()
 
         # #################################################################
         # ################ STARTING THE JOB ###############################
@@ -510,13 +682,13 @@ class Film(FlatCAMTool):
         if self.film_type.get_value() == "pos":
 
             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:
-                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:
-            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 ...")
 
         scale_factor_x = None
@@ -541,29 +713,40 @@ class Film(FlatCAMTool):
         if self.film_mirror_cb.get_value():
             if self.film_mirror_axis.get_value() != 'none':
                 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:
             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:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export positive film"))
 
         filename = 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
         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)
 
@@ -572,7 +755,7 @@ class Film(FlatCAMTool):
 
             try:
                 exc_name = self.exc_combo.currentText()
-            except Exception as e:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                 return
@@ -587,7 +770,7 @@ class Film(FlatCAMTool):
             outname = name + "_punched"
             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:
             log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
 
@@ -638,9 +821,9 @@ class Film(FlatCAMTool):
             outname = name + "_punched"
             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 ...")
 
         scale_factor_x = None
@@ -671,27 +854,386 @@ class Film(FlatCAMTool):
         if border is None:
             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:
             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:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export negative film"))
 
         filename = 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
         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):
         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):
         FlatCAMTool.__init__(self, app)
 
+        self.app = app
+        self.decimals = self.app.decimals
+
         # Title
         title_label = QtWidgets.QLabel("%s" % _('Image to PCB'))
         title_label.setStyleSheet("""
@@ -59,6 +62,7 @@ class ToolImage(FlatCAMTool):
 
         # DPI value of the imported image
         self.dpi_entry = FCSpinner()
+        self.dpi_entry.set_range(0, 99999)
         self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
         self.dpi_label.setToolTip(_("Specify a DPI value for the image.") )
         ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
@@ -145,8 +149,11 @@ class ToolImage(FlatCAMTool):
 
         self.layout.addStretch()
 
+        self.on_image_type(val=False)
+
         # ## Signals
         self.import_button.clicked.connect(self.on_file_importimage)
+        self.image_type.activated_custom.connect(self.on_image_type)
 
     def run(self, toggle=True):
         self.app.report_usage("ToolImage()")
@@ -187,6 +194,28 @@ class ToolImage(FlatCAMTool):
         self.mask_g_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):
         """
         Callback for menu item File->Import IMAGE.
@@ -194,7 +223,7 @@ class ToolImage(FlatCAMTool):
         :type type_of_obj: str
         :return: None
         """
-        mask = []
+        mask = list()
         self.app.log.debug("on_file_importimage()")
 
         _filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
@@ -218,6 +247,52 @@ class ToolImage(FlatCAMTool):
         if filename == "":
             self.app.inform.emit(_("Open cancelled."))
         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]})
-            #  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):
         FlatCAMTool.__init__(self, app)
+        self.app = app
+        self.decimals = self.app.decimals
 
         self.layout.setContentsMargins(0, 0, 3, 0)
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
@@ -160,11 +162,12 @@ class ToolMove(FlatCAMTool):
 
                     def job_move(app_obj):
                         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
                                 for sel_obj in obj_list:
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
@@ -173,10 +176,9 @@ class ToolMove(FlatCAMTool):
 
                                     try:
                                         sel_obj.replotApertures.emit()
-                                    except Exception as e:
+                                    except Exception:
                                         pass
 
-                                for sel_obj in obj_list:
                                     # offset solid_geometry
                                     sel_obj.offset((dx, dy))
 
@@ -186,15 +188,13 @@ class ToolMove(FlatCAMTool):
                                     sel_obj.options['ymin'] = b
                                     sel_obj.options['xmax'] = c
                                     sel_obj.options['ymax'] = d
-
-                                # time to plot the moved objects
-                                self.replot_signal.emit(obj_list)
                             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"
 
-                        proc.done()
+                            # time to plot the moved objects
+                            app_obj.replot_signal.emit(obj_list)
+
                         # delete the selection bounding box
                         self.delete_shape()
                         self.app.inform.emit('[success] %s %s' %
@@ -314,7 +314,7 @@ class ToolMove(FlatCAMTool):
 
     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 = proc_shape.buffer(0.2)
         else:

+ 158 - 111
flatcamTools/ToolNonCopperClear.py

@@ -39,7 +39,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
     def __init__(self, app):
         self.app = app
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         FlatCAMTool.__init__(self, app)
         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"
               "- 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
         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")
         )
 
+        grid1.addWidget(self.milling_type_label, 0, 0)
+        grid1.addWidget(self.milling_type_radio, 0, 1)
+
         # 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"
@@ -191,9 +196,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                           "WARNING: using rest machining will automatically set the order\n"
                                           "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_radio.hide()
@@ -202,7 +207,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # ############### 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
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
@@ -219,17 +224,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "- 'V-shape'\n"
               "- 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
         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.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
         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.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()
         self.tools_box.addLayout(grid2)
@@ -287,40 +315,21 @@ class NonCopperClear(FlatCAMTool, Gerber):
         e_lab_1 = QtWidgets.QLabel('<b>%s:</b>' % _("Parameters"))
         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
         nccoverlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         nccoverlabel.setToolTip(
             _("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"
               "and increasing it if areas that should be cleared are still \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"
               "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.setRange(0.000, 0.999)
+        self.ncc_overlap_entry.setRange(0.000, 99.9999)
         self.ncc_overlap_entry.setSingleStep(0.1)
         grid3.addWidget(nccoverlabel, 2, 0)
         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.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':
             self.ncc_offset_spinner.setSingleStep(0.1)
         else:
@@ -429,12 +438,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         ], orientation='vertical', stretch=False)
         self.reference_label = QtWidgets.QLabel(_("Reference:"))
         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"
-              "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_radio, 10, 1)
@@ -448,9 +454,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "It can be Gerber, Excellon or Geometry.")
         )
         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)
 
         self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
@@ -473,8 +479,27 @@ class NonCopperClear(FlatCAMTool, Gerber):
             _("Create the Geometry Object\n"
               "for non-copper routing.")
         )
+        self.generate_ncc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.generate_ncc_button)
         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 ###################################
         # #############################################################################
 
@@ -530,6 +555,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
 
+        self.tooldia = None
+
         # #############################################################################
         # ############################ SGINALS ########################################
         # #############################################################################
@@ -538,6 +565,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         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.reference_radio.group_toggle_fn = self.on_toggle_reference
         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.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):
         obj_type = self.type_obj_combo.currentIndex()
@@ -554,7 +586,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
     def on_add_tool_by_key(self):
         tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
                                        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'))
 
         val, ok = tool_add_popup.get_value()
@@ -605,7 +637,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
 
     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":
             self.decimals = 4
@@ -627,6 +659,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tool_type_radio.set_value(self.app.defaults["tools_ncctool_type"])
         self.tipdia_entry.set_value(self.app.defaults["tools_ncctipdia"])
         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())
 
@@ -702,18 +735,13 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.bound_obj = None
 
         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):
         self.ui_disconnect()
 
         # 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 = []
         for k, v in self.ncc_tools.items():
@@ -908,54 +936,50 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
     def on_tool_type(self, val):
         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.tipdia_entry.show()
             self.tipanglelabel.show()
             self.tipangle_entry.show()
         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.tipdia_entry.hide()
             self.tipanglelabel.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:
                 self.build_ui()
                 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.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"])
 
-        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.contour = self.ncc_contour_cb.get_value()
         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)
         except Exception as e:
             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:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
@@ -1174,8 +1193,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
             try:
                 self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name)
             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,
                               ncctooldia=self.ncc_dia_list,
@@ -1258,13 +1277,20 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 pt3 = (x1, 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
                 return
 
         elif event.button == right_button and self.mouse_is_dragging == 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)
@@ -1393,7 +1419,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # ####### 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.")
         self.app.inform.emit(_("NCC Tool started. Reading parameters."))
@@ -1410,7 +1436,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         else:
             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"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
@@ -1598,11 +1624,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     sol_geo = ncc_obj.solid_geometry
 
                 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)
-                    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)
                 if empty == 'fail':
                     return 'fail'
@@ -1769,7 +1794,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
                 # variables to display the percentage of work done
                 geo_len = len(area.geoms)
-                disp_number = 0
+
                 old_disp_number = 0
                 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)
 
             geo_obj.options["cnctooldia"] = str(tool)
+
             geo_obj.multigeo = True
             geo_obj.tools.clear()
             geo_obj.tools = dict(tools_storage)
@@ -1895,6 +1921,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                                      "Change the painting parameters and try again."))
                 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...
             # print("Indexing...", end=' ')
             # 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 "
                                                     "isolation is broken for"), str(warning_flag), _("tools")))
                 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:
                 # 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
@@ -2291,11 +2337,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 if run_threaded:
                     proc.done()
                 return
-            except Exception as e:
+            except Exception:
                 if run_threaded:
                     proc.done()
                 traceback.print_stack()
                 return
+
             if run_threaded:
                 proc.done()
             else:

+ 36 - 4
flatcamTools/ToolOptimal.py

@@ -39,8 +39,8 @@ class ToolOptimal(FlatCAMTool):
     def __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 ##################################
@@ -116,6 +116,9 @@ class ToolOptimal(FlatCAMTool):
 
         # Locations where minimum was found
         self.locations_textb = FCTextArea(parent=self)
+        self.locations_textb.setPlaceholderText(
+            _("Coordinates for points where minimum distance was found.")
+        )
         self.locations_textb.setReadOnly(True)
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
@@ -164,6 +167,10 @@ class ToolOptimal(FlatCAMTool):
 
         # Other distances
         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)
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
@@ -184,6 +191,10 @@ class ToolOptimal(FlatCAMTool):
 
         # Locations where minimum was found
         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)
         stylesheet = """
                         QTextEdit { selection-background-color:blue;
@@ -211,9 +222,30 @@ class ToolOptimal(FlatCAMTool):
               "this will allow the determination of the right tool to\n"
               "use for isolation or copper clearing.")
         )
+        self.calculate_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.calculate_button.setMinimumWidth(60)
         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.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
         # ################## Finished GUI creation ###################################
@@ -242,7 +274,7 @@ class ToolOptimal(FlatCAMTool):
         self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_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):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
@@ -297,7 +329,7 @@ class ToolOptimal(FlatCAMTool):
         self.reset_fields()
 
     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())
 
         selection_index = self.gerber_object_combo.currentIndex()

+ 3 - 2
flatcamTools/ToolPDF.py

@@ -44,6 +44,7 @@ class ToolPDF(FlatCAMTool):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         self.app = app
+        self.decimals = self.app.decimals
         self.step_per_circles = self.app.defaults["gerber_circle_steps"]
 
         self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
@@ -134,7 +135,7 @@ class ToolPDF(FlatCAMTool):
         self.on_open_pdf_click()
 
     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):
         pass
@@ -180,7 +181,7 @@ class ToolPDF(FlatCAMTool):
         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)
-        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
             self.point_to_unit_factor = 25.4 / 72
         else:

+ 226 - 175
flatcamTools/ToolPaint.py

@@ -42,7 +42,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
     def __init__(self, app):
         self.app = app
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         FlatCAMTool.__init__(self, app)
         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.setToolTip(
             _("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"
               "and increasing it if areas that should be painted are still \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"
               "due of too many paths.")
         )
-        self.paintoverlap_entry = FCDoubleSpinner()
+        self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
         self.paintoverlap_entry.set_precision(3)
         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)
         grid3.addWidget(ovlabel, 1, 0)
         grid3.addWidget(self.paintoverlap_entry, 1, 1)
@@ -301,27 +299,29 @@ class ToolPaint(FlatCAMTool, Gerber):
         # Polygon selection
         selectlabel = QtWidgets.QLabel('%s:' % _('Selection'))
         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"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\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.")
         )
         grid3.addWidget(selectlabel, 7, 0)
         # grid3 = QtWidgets.QGridLayout()
         self.selectmethod_combo = RadioSet([
-            {"label": _("Single Polygon"), "value": "single"},
+            {"label": _("Polygon Selection"), "value": "single"},
             {"label": _("Area Selection"), "value": "area"},
             {"label": _("All Polygons"), "value": "all"},
             {"label": _("Reference Object"), "value": "ref"}
         ], orientation='vertical', stretch=False)
         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"
               "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\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.")
         )
         grid3.addWidget(self.selectmethod_combo, 7, 1)
@@ -335,9 +335,9 @@ class ToolPaint(FlatCAMTool, Gerber):
               "It can be Gerber, Excellon or Geometry.")
         )
         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)
 
         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"
               "specified by another object.")
         )
+        self.generate_paint_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.generate_paint_button)
 
         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 #####################################
         # #######################################################################################
 
@@ -381,6 +401,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.overlap = None
         self.connect = None
         self.contour = None
+        self.select_method = None
 
         self.units = ''
         self.paint_tools = {}
@@ -395,6 +416,12 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         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
         self.default_data = {}
         self.default_data.update({
@@ -446,6 +473,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
         # #############################################################################
         # ###################### Setup CONTEXT MENU ###################################
@@ -575,7 +603,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         # make the default object type, "Geometry"
         self.type_obj_combo.setCurrentIndex(2)
         # 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":
             self.decimals = 4
@@ -637,7 +665,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             pass
 
         # 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 = []
         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.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.contour = self.paintcontour_cb.get_value()
         self.select_method = self.selectmethod_combo.get_value()
-
         self.obj_name = self.obj_combo.currentText()
 
         # Get source object.
@@ -1004,8 +1023,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         continue
                 self.tooldia_list.append(self.tooldia)
         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
 
         if self.select_method == "all":
@@ -1017,36 +1035,16 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 contour=self.contour)
 
         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:
                 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:
                 self.app.plotcanvas.graph_event_disconnect(self.app.mr)
                 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":
-            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:
                 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.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
-
         elif self.select_method == 'ref':
             self.bound_obj_name = self.box_combo.currentText()
             # Get source object.
@@ -1091,6 +1086,91 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 connect=self.connect,
                                 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.
     def on_mouse_release(self, event):
         if self.app.is_legacy is False:
@@ -1134,13 +1214,21 @@ class ToolPaint(FlatCAMTool, Gerber):
                 pt2 = (x1, y0)
                 pt3 = (x1, 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
                 return
 
         elif event.button == right_button and self.mouse_is_dragging is 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)
@@ -1220,6 +1308,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
     def paint_poly(self, obj,
                    inside_pt=None,
+                   poly_list=None,
                    tooldia=None,
                    overlap=None,
                    order=None,
@@ -1251,19 +1340,15 @@ class ToolPaint(FlatCAMTool, Gerber):
         :return: None
         """
 
-        # Which polygon.
-        # poly = find_polygon(self.solid_geometry, inside_pt)
         if isinstance(obj, FlatCAMGerber):
             if self.app.defaults["gerber_buffering"] == 'no':
                 self.app.inform.emit('%s %s %s' %
                                      (_("Paint Tool."), _("Normal painting polygon task started."),
                                       _("Buffering geometry...")))
             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:
-            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 self.app.defaults["tools_paint_plotting"] == 'progressive':
@@ -1272,44 +1357,29 @@ class ToolPaint(FlatCAMTool, Gerber):
                 else:
                     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?
-        if poly is None:
+        if polygon_list is None:
             self.app.log.warning('No polygon found.')
             self.app.inform.emit('[WARNING] %s' % _('No polygon found.'))
             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"
-
         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"]
         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()
+        tools_storage = self.paint_tools if tools_storage is None else tools_storage
 
         sorted_tools = []
         if tooldia is not None:
@@ -1324,24 +1394,17 @@ class ToolPaint(FlatCAMTool, Gerber):
             for row in range(self.tools_table.rowCount()):
                 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
         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):
                 cpoly = None
@@ -1388,21 +1451,8 @@ class ToolPaint(FlatCAMTool, Gerber):
                                         _('Geometry could not be painted completely'))
                     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)
-
-            geo_obj.solid_geometry = []
-
+            tool_dia = None
             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
                 for k, v in tools_storage.items():
@@ -1410,68 +1460,77 @@ class ToolPaint(FlatCAMTool, Gerber):
                         current_uid = int(k)
                         break
 
+            try:
+                poly_buf = [pol.buffer(-paint_margin) for pol in polygon_list]
+                cp = list()
                 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
             if self.app.defaults["tools_paint_plotting"] == 'progressive':
                 self.temp_shapes.clear(update=True)
 
             # delete tools with empty geometry
-            keys_to_delete = []
             # 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 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)
-            # 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.multitool = True
             geo_obj.tools.clear()
             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
             has_solid_geo = 0
             for tooluid in geo_obj.tools:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
+
             if has_solid_geo == 0:
                 self.app.inform.emit('[ERROR] %s' %
                                      _("There is no Painting Geometry in the file.\n"
@@ -1479,6 +1538,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                        "Change the painting parameters and try again."))
                 return
 
+            total_geometry[:] = []
             self.app.inform.emit('[success] %s' % _("Paint Single Done."))
 
             # 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.
         :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:
             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
+            paint_margin = float(self.paintmargin_entry.get_value())
 
         # determine if to use the progressive plotting
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
@@ -1979,7 +2030,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             except FlatCAMApp.GracefulException:
                 proc.done()
                 return
-            except Exception as e:
+            except Exception:
                 proc.done()
                 traceback.print_stack()
                 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.
         :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:
             paint_margin = margin
@@ -2454,7 +2505,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             except FlatCAMApp.GracefulException:
                 proc.done()
                 return
-            except Exception as e:
+            except Exception:
                 proc.done()
                 traceback.print_stack()
                 return

+ 24 - 7
flatcamTools/ToolPanelize.py

@@ -34,8 +34,9 @@ class Panelize(FlatCAMTool):
     toolName = _("Panelize PCB")
 
     def __init__(self, app):
-        super(Panelize, self).__init__(self)
-        self.app = app
+        self.decimals = app.decimals
+
+        FlatCAMTool.__init__(self, app)
 
         # ## Title
         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])
 
         # 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.setToolTip(
             _("Panelize the specified object around the specified box.\n"
               "In other words it creates multiple copies of the source object,\n"
               "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()
 
+        # ## 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
         self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
         self.panelize_object_button.clicked.connect(self.on_panelize)
         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.reset_button.clicked.connect(self.set_tool_ui)
 
         # list to hold the temporary objects
         self.objs = []

+ 1 - 0
flatcamTools/ToolPcbWizard.py

@@ -34,6 +34,7 @@ class PcbWizard(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
 
         self.app = app
+        self.decimals = self.app.decimals
 
         # Title
         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 FlatCAMTool import FlatCAMTool
-from FlatCAMObj import FlatCAMCNCjob
 
 from shapely.geometry import MultiPolygon, Polygon
 from shapely.ops import cascaded_union
 
 from copy import deepcopy
+import math
+
 import logging
 import gettext
 import FlatCAMTranslation as fcTranslate
@@ -28,13 +29,15 @@ log = logging.getLogger('base')
 class Properties(FlatCAMTool):
     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):
         FlatCAMTool.__init__(self, app)
 
         self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
 
+        self.decimals = self.app.decimals
+
         # this way I can hide/show the frame
         self.properties_frame = QtWidgets.QFrame()
         self.properties_frame.setContentsMargins(0, 0, 0, 0)
@@ -113,39 +116,57 @@ class Properties(FlatCAMTool):
     def properties(self):
         obj_list = self.app.collection.get_selected()
         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.properties_frame.hide()
             self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
             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:
             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"))
 
     def addItems(self, obj):
         parent = self.treeWidget.invisibleRootItem()
         apertures = ''
         tools = ''
+        drills = ''
+        slots = ''
+        others = ''
 
         font = QtGui.QFont()
         font.setBold(True)
+
+        # main Items categories
         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)
         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)
-
         options = self.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
+
         if obj.kind.lower() == 'gerber':
             apertures = self.addParent(parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         else:
             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, '')
 
-        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:
             self.addChild(obj_type,
                           ['%s:' % _('Geo Type'),
@@ -162,6 +183,7 @@ class Properties(FlatCAMTool):
             length = 0.0
             width = 0.0
             area = 0.0
+            copper_area = 0.0
 
             geo = obj_prop.solid_geometry
             if geo:
@@ -172,26 +194,56 @@ class Properties(FlatCAMTool):
                     length = abs(xmax - xmin)
                     width = abs(ymax - ymin)
                 except Exception as e:
-                    log.debug("PropertiesTool.addItems() --> %s" % str(e))
+                    log.debug("PropertiesTool.addItems() -> calculate dimensions --> %s" % str(e))
 
                 # 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
                 else:
                     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:
                 xmin = []
                 ymin = []
                 xmax = []
                 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:
-                        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:
                         log.debug("PropertiesTool.addItems() --> %s" % str(ee))
 
@@ -205,15 +257,32 @@ class Properties(FlatCAMTool):
                     width = abs(ymax - ymin)
 
                     # 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
                     else:
                         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:
                     log.debug("Properties.addItems() --> %s" % str(e))
 
             area_chull = 0.0
-            if not isinstance(obj_prop, FlatCAMCNCjob):
+            if obj_prop.kind.lower() != 'cncjob':
                 # calculate and add convex hull area
                 if geo:
                     if isinstance(geo, MultiPolygon):
@@ -238,29 +307,35 @@ class Properties(FlatCAMTool):
                         area_chull = None
                         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
 
-            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.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:
             if option is 'name':
                 continue
             self.addChild(options, [str(option), str(obj.options[option])], True)
 
+        # Items that depend on the object type
         if obj.kind.lower() == 'gerber':
             temp_ap = dict()
             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)
                 for key in temp_ap:
                     self.addChild(apid, [str(key), str(temp_ap[key])], True)
-
         elif obj.kind.lower() == 'excellon':
+            tot_drill_cnt = 0
+            tot_slot_cnt = 0
+
             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':
             for tool, value in obj.tools.items():
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
@@ -310,26 +418,110 @@ class Properties(FlatCAMTool):
                     else:
                         self.addChild(geo_tool, [str(k), str(v)], True)
         elif obj.kind.lower() == 'cncjob':
+            # for cncjob objects made from gerber or geometry
             for tool, value in obj.cnc_tools.items():
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                     if k == 'solid_geometry':
                         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':
                         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':
                         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':
-                        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():
-                            self.addChild(tool_data, [str(data_k), str(data_v)], True)
+                            self.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
                     else:
                         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, [''])
 
     def addParent(self, parent, title, expanded=False, color=None, font=None):
@@ -343,27 +535,53 @@ class Properties(FlatCAMTool):
             item.setFont(0, font)
         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.setText(0, str(title[0]))
         if column1 is not None:
             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
-        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
-        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:
-            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

+ 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)
 
     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_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"
               "arranged in a 2D array of rows and columns.")
         )
+        self.run_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         hlay_2.addWidget(self.run_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)
+
         # #######################################################
         # ################ SIGNALS ##############################
         # #######################################################
@@ -520,6 +539,7 @@ class RulesCheck(FlatCAMTool):
         # self.app.collection.rowsInserted.connect(self.on_object_loaded)
 
         self.tool_finished.connect(self.on_tool_finished)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
         # list to hold the temporary objects
         self.objs = []
@@ -1160,7 +1180,7 @@ class RulesCheck(FlatCAMTool):
                     return
 
             # 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()
                 bottom_dict = dict()
                 outline_dict = dict()

+ 128 - 38
flatcamTools/ToolSolderPaste.py

@@ -7,10 +7,11 @@
 
 from FlatCAMTool import FlatCAMTool
 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 camlib import distance
 from FlatCAMObj import FlatCAMCNCjob
+from flatcamEditors.FlatCAMTextEditor import TextEditor
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt
@@ -38,6 +39,9 @@ class SolderPaste(FlatCAMTool):
     def __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_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
@@ -101,7 +105,10 @@ class SolderPaste(FlatCAMTool):
         self.addtool_entry_lbl.setToolTip(
             _("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.addStretch()
@@ -127,6 +134,12 @@ class SolderPaste(FlatCAMTool):
         self.soldergeo_btn.setToolTip(
             _("Generate solder paste dispensing geometry.")
         )
+        self.soldergeo_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
         grid0.addWidget(self.addtool_btn, 0, 0)
         # grid2.addWidget(self.copytool_btn, 0, 1)
@@ -161,7 +174,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_box.addLayout(self.gcode_form_layout)
 
         # 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.setToolTip(
             _("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)
 
         # 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.setToolTip(
             _("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)
 
         # 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.setToolTip(
             _("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)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
             _("Feedrate (speed) while moving vertically\n"
@@ -228,7 +269,11 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.frz_label, self.frz_entry)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
             _("Pause after solder dispensing.")
@@ -254,7 +306,10 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry)
 
         # 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.setToolTip(
            _("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)
 
         # 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.setToolTip(
             _("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)
 
-        # Postprocessors
+        # Preprocessors
         pp_label = QtWidgets.QLabel('%s:' % _('PostProcessor'))
         pp_label.setToolTip(
             _("Files that control the GCode generation.")
@@ -290,6 +349,12 @@ class SolderPaste(FlatCAMTool):
            _("Generate GCode for Solder Paste dispensing\n"
              "on PCB pads.")
         )
+        self.solder_gcode_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
         self.generation_frame = QtWidgets.QFrame()
         self.generation_frame.setContentsMargins(0, 0, 0, 0)
@@ -370,12 +435,24 @@ class SolderPaste(FlatCAMTool):
             _("View the generated GCode for Solder Paste dispensing\n"
               "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.setToolTip(
            _("Save the generated GCode for Solder Paste dispensing\n"
              "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.setToolTip(
@@ -385,10 +462,23 @@ class SolderPaste(FlatCAMTool):
 
         grid4.addWidget(step4_lbl, 0, 0)
         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()
 
+        # ## 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.save_gcode_frame.setDisabled(True)
 
@@ -401,8 +491,7 @@ class SolderPaste(FlatCAMTool):
         self.units = ''
         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
         self.obj_to_be_deleted_name = ''
@@ -416,7 +505,7 @@ class SolderPaste(FlatCAMTool):
         # ## Signals
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         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.soldergeo_btn.clicked.connect(self.on_create_geo_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.app.object_status_changed.connect(self.update_comboboxes)
+        self.reset_button.clicked.connect(self.set_tool_ui)
 
     def run(self, toggle=True):
         self.app.report_usage("ToolSolderPaste()")
@@ -505,7 +595,7 @@ class SolderPaste(FlatCAMTool):
 
         try:
             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. "
                       "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.")
             return
@@ -526,15 +616,15 @@ class SolderPaste(FlatCAMTool):
         self.name = ""
         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":
             self.decimals = 4
         else:
             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':
                 continue
             self.pp_combo.addItem(name)
@@ -549,7 +639,7 @@ class SolderPaste(FlatCAMTool):
         self.ui_disconnect()
 
         # 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 = []
         for k, v in self.tooltable_tools.items():
@@ -623,7 +713,7 @@ class SolderPaste(FlatCAMTool):
         if row is None:
             try:
                 current_row = self.tools_table.currentRow()
-            except:
+            except Exception:
                 current_row = 0
         else:
             current_row = row
@@ -1235,9 +1325,9 @@ class SolderPaste(FlatCAMTool):
             xmax = obj.options['xmax']
             ymax = obj.options['ymax']
         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 += 'FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s' % str(e)
+            msg += 'SolderPaste.on_create_gcode() --> %s' % str(e)
             msg += traceback.format_exc()
             self.app.inform.emit(msg)
             return
@@ -1325,14 +1415,14 @@ class SolderPaste(FlatCAMTool):
         """
         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
-        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()
         obj = self.app.collection.get_by_name(name)
@@ -1376,21 +1466,21 @@ class SolderPaste(FlatCAMTool):
         try:
             for line in lines:
                 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:
             log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e))
             self.app.inform.emit('[ERROR] %s --> %s' %
                                  ('ToolSolderPaste.on_view_gcode()', str(e)))
             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):
         """
-        Save sodlerpaste dispensing GCode to a file on HDD.
+        Save solderpaste dispensing GCode to a file on HDD.
 
         :return:
         """
@@ -1405,7 +1495,7 @@ class SolderPaste(FlatCAMTool):
             return
 
         _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:
             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):
         self.app = app
+        self.decimals = self.app.decimals
 
         FlatCAMTool.__init__(self, app)
 
@@ -100,6 +101,12 @@ class ToolSub(FlatCAMTool):
               "Can be used to remove the overlapping silkscreen\n"
               "over the soldermask.")
         )
+        self.intersect_btn.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         self.tools_box.addWidget(self.intersect_btn)
         self.tools_box.addWidget(e_lab_1)
 
@@ -148,11 +155,30 @@ class ToolSub(FlatCAMTool):
             _("Will remove the area occupied by the subtractor\n"
               "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(e_lab_1)
 
         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
         self.check_thread = QtCore.QTimer()
         # 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
         self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
         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):
         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()
         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
 
+        self.app.inform.emit('%s' % _("Loading geometry from Gerber objects."))
+
         # Get target object.
         try:
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
         except Exception as 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
 
         self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
         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
 
         # 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)
         except Exception as 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
 
         # crate the new_apertures dict structure
         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]['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:
             if 'geometry' in self.sub_grb_obj.apertures[apid1]:
@@ -297,6 +322,7 @@ class ToolSub(FlatCAMTool):
                     if 'clear' in elem:
                         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_follow_union = cascaded_union(geo_follow_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:
             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):
-        new_geometry = []
+        new_geometry = list()
 
         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:
                 new_el = dict()
 
@@ -378,6 +404,8 @@ class ToolSub(FlatCAMTool):
 
                 new_geometry.append(deepcopy(new_el))
 
+        self.app.inform.emit('%s: %s...' % (_("Finished parsing geometry for aperture"), str(apid)))
+
         if new_geometry:
             while not self.new_apertures[apid]['geometry']:
                 self.new_apertures[apid]['geometry'] = deepcopy(new_geometry)
@@ -412,6 +440,7 @@ class ToolSub(FlatCAMTool):
                 poly_buff = work_poly_buff.buffer(0.0000001)
             except ValueError:
                 pass
+
             try:
                 poly_buff = poly_buff.buffer(-0.0000001)
             except ValueError:

+ 2 - 2
flatcamTools/ToolTransform.py

@@ -30,7 +30,7 @@ class ToolTransform(FlatCAMTool):
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
-        self.decimals = 4
+        self.decimals = self.app.decimals
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
@@ -371,7 +371,7 @@ class ToolTransform(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
     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):
         # ## Initialize form

+ 5 - 0
flatcamTools/__init__.py

@@ -2,6 +2,7 @@ import sys
 
 
 from flatcamTools.ToolCalculators import ToolCalculator
+from flatcamTools.ToolCalibration import ToolCalibration
 from flatcamTools.ToolCutOut import CutOut
 
 from flatcamTools.ToolDblSided import DblSidedTool
@@ -25,8 +26,12 @@ from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
 from flatcamTools.ToolProperties import Properties
 
+from flatcamTools.ToolQRCode import QRCode
 from flatcamTools.ToolRulesCheck import RulesCheck
 
+from flatcamTools.ToolCopperThieving import ToolCopperThieving
+from flatcamTools.ToolFiducials import ToolFiducials
+
 from flatcamTools.ToolShell import FCShell
 from flatcamTools.ToolSolderPaste import SolderPaste
 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._coo.pyd
 
-import os, site, sys, platform
+import os
+import site
+import sys
+import platform
 from cx_Freeze import setup, Executable
 
 # 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(("locale", "lib/locale"))
-include_files.append(("postprocessors", "lib/postprocessors"))
+include_files.append(("preprocessors", "lib/preprocessors"))
 include_files.append(("share", "lib/share"))
 include_files.append(("flatcamGUI/VisPyData", "lib/vispy"))
 include_files.append(("config", "lib/config"))
@@ -71,11 +74,10 @@ if sys.platform == "win32":
 if platform.architecture()[0] == '64bit':
     buildOptions = dict(
         include_files=include_files,
-        excludes=['scipy','pytz'],
+        excludes=['scipy', 'pytz'],
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # 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:
     buildOptions = dict(
@@ -83,13 +85,26 @@ else:
         excludes=['scipy', 'pytz'],
         # packages=['OpenGL','numpy','vispy','ortools','google']
         # 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)
 
-# 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(
     name="FlatCAM",
@@ -97,5 +112,5 @@ setup(
     version="8.9",
     description="FlatCAM: 2D Computer Aided PCB Manufacturing",
     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 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:
-            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 += '(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:
             x_toolchange = toolchangexy[0]
             y_toolchange = toolchangexy[1]
-
-        if p.units.upper() == 'MM':
-            toolC_formatted = format(float(p['toolC']), '.2f')
         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:
             gcode = """
@@ -88,6 +89,7 @@ T{tool}
 M6    
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 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),
@@ -101,6 +103,7 @@ T{tool}
 M6    
 (MSG, Change to Tool with Nozzle Dia = {toolC})
 M0
+G00 Z{z_toolchange}
 """.format(z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
            tool=int(int(p.tool)),
            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'
 
         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:
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
 
@@ -54,9 +55,9 @@ class Repetier(FlatCAMPostProc):
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
 
         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:
-            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 += ';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)
 
     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):
         z_toolchange = p.z_toolchange
@@ -95,10 +96,7 @@ class Repetier(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
             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':
             for i in p['options']['Tools_in_use']:
@@ -111,6 +109,7 @@ G0 Z{z_toolchange}
 G0 X{x_toolchange} Y{y_toolchange}                
 M84
 @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),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -122,6 +121,7 @@ M84
 G0 Z{z_toolchange}
 M84
 @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),
            tool=int(p.tool),
            t_drills=no_drills,
@@ -139,6 +139,7 @@ G0 Z{z_toolchange}
 G0 X{x_toolchange} Y{y_toolchange}
 M84
 @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),
            y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
            z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -149,6 +150,7 @@ M84
 G0 Z{z_toolchange}
 M84
 @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),
            tool=int(p.tool),
            toolC=toolC_formatted)
@@ -169,7 +171,7 @@ M84
         return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(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):
         coords_xy = p['xy_toolchange']
@@ -183,9 +185,12 @@ M84
     def feedrate_code(self, p):
         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)
 
+    def inline_z_feedrate_code(self, p):
+        return 'F' + self.feedrate_format %(p.fr_decimals, p.z_feedrate)
+
     def z_feedrate_code(self, p):
         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 *
 
 
-# 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.
 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'
 
         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:
             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'
 
         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:
-            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 += '(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:
             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':
             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'
 
         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:
             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'
 
         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:
-            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 += '(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:
             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':
             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 Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         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:
             gcode += '(X,Y Toolchange: ' + "None" + 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'
 
         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:
-            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 += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
@@ -88,19 +89,13 @@ class Toolchange_manual(FlatCAMPostProc):
         if toolchangexy is not None:
             x_toolchange = toolchangexy[0]
             y_toolchange = toolchangexy[1]
-        # else:
-        #     x_toolchange = p.oldx
-        #     y_toolchange = p.oldy
 
         no_drills = 1
 
         if int(p.tool) == 1 and p.startz is not None:
             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':
             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'
 
         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:
             gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
 
@@ -53,9 +54,9 @@ class default(FlatCAMPostProc):
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
         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:
-            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 += '(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:
             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':
             for i in p['options']['Tools_in_use']:
@@ -109,11 +107,12 @@ class default(FlatCAMPostProc):
                 gcode = """
 M5
 G00 Z{z_toolchange}
-G00 X{x_toolchange} Y{y_toolchange}                
 T{tool}
+G00 X{x_toolchange} Y{y_toolchange}                
 M6
 (MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 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),
@@ -127,10 +126,13 @@ 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)
+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:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
             return gcode
@@ -144,11 +146,14 @@ 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)
+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:
                 gcode = """
 M5
@@ -156,9 +161,11 @@ 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)
+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:
                 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 Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         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:
             gcode += '(X,Y Toolchange: ' + "None" + 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'
 
         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:
-            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 += '(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:
             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':
             for i in p['options']['Tools_in_use']:
@@ -112,12 +110,14 @@ 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)
+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:
                 gcode = """
 M5             
@@ -125,10 +125,12 @@ 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)
+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:
                 gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
@@ -143,11 +145,13 @@ 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)
+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:
                 gcode = """
 M5             
@@ -155,9 +159,11 @@ 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)
+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:
                 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'
 
         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:
-            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 += '(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 *
 
 
-# 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.
 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 Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
         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:
             gcode += '(X,Y Toolchange: ' + "None" + 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'
 
         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:
-            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 += '(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:
             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':
             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            #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMPostProc import *
 
@@ -45,7 +45,8 @@ class marlin(FlatCAMPostProc):
         gcode += ';Z Toolchange: ' + str(p['z_toolchange']) + units + '\n'
 
         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:
             gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
 
@@ -54,9 +55,9 @@ class marlin(FlatCAMPostProc):
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
 
         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:
-            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 += ';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)
 
     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):
         z_toolchange = p.z_toolchange
@@ -95,10 +96,7 @@ class marlin(FlatCAMPostProc):
         if int(p.tool) == 1 and p.startz is not None:
             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':
             for i in p['options']['Tools_in_use']:
@@ -113,12 +111,14 @@ G0 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)
+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:
                 gcode = """
 M5
@@ -126,10 +126,12 @@ G0 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)
+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:
                 gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
@@ -144,11 +146,13 @@ G0 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)
+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:
                 gcode = """
 M5
@@ -156,9 +160,11 @@ G0 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)
+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:
                 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)
 
     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):
         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):
         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)
 
+    def inline_z_feedrate_code(self, p):
+        return 'F' + self.feedrate_format %(p.fr_decimals, p.z_feedrate)
+
     def z_feedrate_code(self, p):
         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
-# 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
 cycler>=0.10
 python-dateutil>=2.1
@@ -11,7 +11,6 @@ setuptools
 dill
 rtree
 pyopengl
-pyopengl-accelerate
 vispy
 ortools
 svg.path
@@ -22,3 +21,6 @@ fontTools
 rasterio
 lxml
 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
 
 
@@ -56,7 +56,7 @@ class TclCommandAddCircle(TclCommand):
 
         try:
             obj = self.app.collection.get_by_name(str(obj_name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % obj_name
         if obj is None:
             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 *
 
 

+ 2 - 1
tclCommands/TclCommandAddPolyline.py

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

+ 2 - 2
tclCommands/TclCommandAddRectangle.py

@@ -1,4 +1,4 @@
-from ObjectCollection import *
+import collections
 from tclCommands.TclCommand import TclCommandSignaled
 
 
@@ -58,7 +58,7 @@ class TclCommandAddRectangle(TclCommandSignaled):
 
         try:
             obj = self.app.collection.get_by_name(str(obj_name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % obj_name
         if obj is None:
             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 FlatCAMObj import FlatCAMGeometry, FlatCAMGerber, FlatCAMExcellon
+
+from shapely.geometry import Point
+import shapely.affinity as affinity
 
 
 class TclCommandAlignDrill(TclCommandSignaled):
@@ -42,6 +46,8 @@ class TclCommandAlignDrill(TclCommandSignaled):
             ('name', 'Name of the object (Gerber or Excellon) to mirror.'),
             ('dia', 'Tool diameter'),
             ('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'
                      'inside table in grid (-5,0),(5,0),(15,0)...'),
             ('gridoffset', 'offset of grid from 0 position.'),
@@ -75,7 +81,7 @@ class TclCommandAlignDrill(TclCommandSignaled):
         # Get source object.
         try:
             obj = self.app.collection.get_by_name(str(name))
-        except:
+        except Exception:
             return "Could not retrieve object: %s" % name
 
         if obj is None:
@@ -173,7 +179,7 @@ class TclCommandAlignDrill(TclCommandSignaled):
         if 'box' in args:
             try:
                 box = self.app.collection.get_by_name(args['box'])
-            except:
+            except Exception:
                 return "Could not retrieve object box: %s" % args['box']
 
             if box is None:

+ 2 - 1
tclCommands/TclCommandAlignDrillGrid.py

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

+ 6 - 2
tclCommands/TclCommandBbox.py

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

+ 3 - 3
tclCommands/TclCommandBounds.py

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

+ 1 - 1
tclCommands/TclCommandClearShell.py

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

+ 9 - 7
tclCommands/TclCommandCncjob.py

@@ -1,5 +1,8 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommandSignaled
+from FlatCAMObj import FlatCAMGeometry
+
+import collections
+from copy import deepcopy
 
 
 class TclCommandCncjob(TclCommandSignaled):
@@ -60,7 +63,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('feedrate', 'Moving speed on X-Y plane when cutting.'),
             ('feedrate_z', 'Moving speed on Z plane 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)'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('toolchange', 'Enable tool changes (example: True).'),
@@ -72,7 +75,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('dwell', 'True or False; use (or not) the dwell'),
             ('dwelltime', 'Time to pause to allow the spindle to reach the full speed'),
             ('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.')
         ]),
         '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 \
             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 \
             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["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["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
 
+import collections
+import logging
+
 import gettext
 import FlatCAMTranslation as fcTranslate
 import builtins
@@ -9,6 +11,8 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
+log = logging.getLogger('base')
+
 
 class TclCommandCopperClear(TclCommand):
     """
@@ -85,6 +89,17 @@ class TclCommandCopperClear(TclCommand):
 
         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:
             tooldia = str(args['tooldia'])
         else:
@@ -111,18 +126,18 @@ class TclCommandCopperClear(TclCommand):
             method = str(self.app.defaults["tools_nccmethod"])
 
         if 'connect' in args:
-            connect = eval(str(args['connect']).capitalize())
+            connect = bool(args['connect'])
         else:
             connect = eval(str(self.app.defaults["tools_nccconnect"]))
 
         if 'contour' in args:
-            contour = eval(str(args['contour']).capitalize())
+            contour = bool(args['contour'])
         else:
             contour = eval(str(self.app.defaults["tools_ncccontour"]))
 
         offset = 0.0
         if 'has_offset' in args:
-            has_offset = args['has_offset']
+            has_offset = bool(args['has_offset'])
             if args['has_offset'] is True:
                 if 'offset' in args:
                     offset = float(args['margin'])
@@ -177,7 +192,7 @@ class TclCommandCopperClear(TclCommand):
             tooluid += 1
             ncc_tools.update({
                 int(tooluid): {
-                    'tooldia': float('%.4f' % tool),
+                    'tooldia': float('%.*f' % (obj.decimals, tool)),
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'type': 'Iso',
@@ -188,7 +203,7 @@ class TclCommandCopperClear(TclCommand):
             })
 
         if 'rest' in args:
-            rest = eval(str(args['rest']).capitalize())
+            rest = bool(args['rest'])
         else:
             rest = eval(str(self.app.defaults["tools_nccrest"]))
 
@@ -200,17 +215,6 @@ class TclCommandCopperClear(TclCommand):
             else:
                 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
         if 'all' in args and args['all'] == 1:
             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
+
+import collections
+import logging
+
 from shapely.ops import cascaded_union
 from shapely.geometry import LineString
 
+log = logging.getLogger('base')
+
 
 class TclCommandCutout(TclCommand):
     """
@@ -62,12 +67,12 @@ class TclCommandCutout(TclCommand):
             return
 
         if 'margin' in args:
-            margin_par = args['margin']
+            margin_par = float(args['margin'])
         else:
             margin_par = 0.001
 
         if 'dia' in args:
-            dia_par = args['dia']
+            dia_par = float(args['dia'])
         else:
             dia_par = 0.1
 
@@ -77,7 +82,7 @@ class TclCommandCutout(TclCommand):
             gaps_par = "4"
 
         if 'gapsize' in args:
-            gapsize_par = args['gapsize']
+            gapsize_par = float(args['gapsize'])
         else:
             gapsize_par = 0.1
 

+ 2 - 1
tclCommands/TclCommandDelete.py

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

+ 16 - 14
tclCommands/TclCommandDrillcncjob.py

@@ -1,5 +1,8 @@
-from ObjectCollection import *
 from tclCommands.TclCommand import TclCommandSignaled
+from FlatCAMObj import FlatCAMExcellon
+
+import collections
+import math
 
 
 class TclCommandDrillcncjob(TclCommandSignaled):
@@ -57,7 +60,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('dwell', 'True or False; use (or not) the dwell'),
             ('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.'),
             ('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 '
@@ -86,17 +89,18 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
         name = args['name']
 
+        obj = self.app.collection.get_by_name(name)
+
         if 'outname' not in args:
             args['outname'] = name + "_cnc"
 
         if 'muted' in args:
-            muted = args['muted']
+            muted = bool(args['muted'])
         else:
-            muted = 0
+            muted = False
 
-        obj = self.app.collection.get_by_name(name)
         if obj is None:
-            if muted == 0:
+            if muted is False:
                 self.raise_tcl_error("Object not found: %s" % name)
             else:
                 return "fail"
@@ -114,20 +118,18 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
         def job_init(job_obj, app_obj):
             # 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:
                 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)
 
                     req_tools = set()
                     for tool in obj.tools:
                         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:
                                 tolerance = args['diatol'] / 100
@@ -173,7 +175,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             toolchangez = args["toolchangez"] if "toolchangez" in args and args["toolchangez"] else \
                 obj.options["toolchangez"]
             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'
 
             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"] \
                 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.dwelltime = float(args['dwelltime'])
 

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