Explorar o código

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

Beta - updates
Marius Stanciu %!s(int64=6) %!d(string=hai) anos
pai
achega
ea00ec4e2f

+ 56 - 23
FlatCAMApp.py

@@ -141,8 +141,8 @@ class App(QtCore.QObject):
     # ##########################################################################
     # ################## Version and VERSION DATE ##############################
     # ##########################################################################
-    version = 8.991
-    version_date = "2019/12/27"
+    version = 8.992
+    version_date = "2020/01/02"
     beta = True
     engine = '3D'
 
@@ -843,6 +843,7 @@ class App(QtCore.QObject):
             "tools_transform_mirror_reference": False,
             "tools_transform_mirror_point": (0, 0),
             "tools_transform_buffer_dis": 0.0,
+            "tools_transform_buffer_factor": 100.0,
             "tools_transform_buffer_corner": True,
 
             # SolderPaste Tool
@@ -1465,6 +1466,7 @@ class App(QtCore.QObject):
             "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb,
             "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry,
             "tools_transform_buffer_dis": self.ui.tools_defaults_form.tools_transform_group.buffer_entry,
+            "tools_transform_buffer_factor": self.ui.tools_defaults_form.tools_transform_group.buffer_factor_entry,
             "tools_transform_buffer_corner": self.ui.tools_defaults_form.tools_transform_group.buffer_rounded_cb,
 
             # SolderPaste Dispensing Tool
@@ -1793,12 +1795,14 @@ class App(QtCore.QObject):
 
             if self.cmd_line_headless == 1:
                 self.trayIcon = FlatCAMSystemTray(app=self,
-                                                  icon=QtGui.QIcon(self.resource_location + '/flatcam_icon32_green.png'),
+                                                  icon=QtGui.QIcon(self.resource_location +
+                                                                   '/flatcam_icon32_green.png'),
                                                   headless=True,
                                                   parent=self.parent_w)
             else:
                 self.trayIcon = FlatCAMSystemTray(app=self,
-                                                  icon=QtGui.QIcon(self.resource_location + '/flatcam_icon32_green.png'),
+                                                  icon=QtGui.QIcon(self.resource_location +
+                                                                   '/flatcam_icon32_green.png'),
                                                   parent=self.parent_w)
 
         # #############################################################################
@@ -2919,7 +2923,6 @@ class App(QtCore.QObject):
             #     self.defaults_form_fields[option].set_value(self.defaults[option])
             # except KeyError:
             #     #self.log.debug("defaults_write_form(): No field for: %s" % option)
-            #     # TODO: Rethink this?
             #     pass
 
     def defaults_write_form_field(self, field, factor=None, units=None, defaults_dict=None):
@@ -2929,6 +2932,7 @@ class App(QtCore.QObject):
         :param field: the GUI element in Preferences GUI to be updated
         :param factor: factor to be applied to the field parameter
         :param units: current FLatCAM measuring units
+        :param defaults_dict: the defaults storage
         :return: None, it updates GUI elements
         """
 
@@ -2949,8 +2953,6 @@ class App(QtCore.QObject):
                 elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
                     self.defaults_form_fields[field].set_value((def_dict[field] * factor))
         except KeyError:
-            # self.log.debug("defaults_write_form(): No field for: %s" % option)
-            # TODO: Rethink this?
             pass
         except AttributeError:
             log.debug(field)
@@ -3258,6 +3260,10 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             self.call_source = 'grb_editor'
 
+            # reset the following variables so the UI is built again after edit
+            edited_object.ui_build = False
+            edited_object.build_aperture_storage = False
+
         # make sure that we can't select another object while in Editor Mode:
         # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
         self.ui.project_frame.setDisabled(True)
@@ -3267,8 +3273,7 @@ class App(QtCore.QObject):
 
         self.ui.plot_tab_area.setTabText(0, "EDITOR Area")
         self.ui.plot_tab_area.protectTab(0)
-        self.inform.emit('[WARNING_NOTCL] %s' %
-                         _("Editor is activated ..."))
+        self.inform.emit('[WARNING_NOTCL] %s' % _("Editor is activated ..."))
 
         self.should_we_save = True
 
@@ -6828,6 +6833,9 @@ class App(QtCore.QObject):
             if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
                 self.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('black'))
 
+        # restore the default stylesheet by setting a blank one
+        self.ui.pref_apply_button.setStyleSheet("")
+
         self.inform.emit('%s' % _("Preferences applied."))
 
         # make sure we update the self.current_defaults dict used to undo changes to self.defaults
@@ -6902,7 +6910,7 @@ class App(QtCore.QObject):
 
         # work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry
         if notebook_widget_name == 'selected_tab':
-            if str(type(self.collection.get_active())) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
+            if self.collection.get_active().kind == 'geometry':
                 # Tool add works for Geometry only if Advanced is True in Preferences
                 if self.defaults["global_app_level"] == 'a':
                     tool_add_popup = FCInputDialog(title="New Tool ...",
@@ -6918,12 +6926,11 @@ class App(QtCore.QObject):
                             return
                         self.collection.get_active().on_tool_add(dia=float(val))
                     else:
-                        self.inform.emit('[WARNING_NOTCL] %s...' %
-                                         _("Adding Tool cancelled"))
+                        self.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
                 else:
                     msgbox = QtWidgets.QMessageBox()
                     msgbox.setText(_("Adding Tool works only when Advanced is checked.\n"
-                                   "Go to Preferences -> General - Show Advanced Options."))
+                                     "Go to Preferences -> General - Show Advanced Options."))
                     msgbox.setWindowTitle("Tool adding ...")
                     msgbox.setWindowIcon(QtGui.QIcon(self.resource_location + '/warning.png'))
                     bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
@@ -7616,13 +7623,16 @@ class App(QtCore.QObject):
                     pass
 
     def on_preferences_edited(self):
-        self.inform.emit('[WARNING_NOTCL] %s' % _("Preferences edited but not saved."))
+        if self.preferences_changed_flag is False:
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Preferences edited but not saved."))
 
-        for idx in range(self.ui.plot_tab_area.count()):
-            if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
-                self.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
+            for idx in range(self.ui.plot_tab_area.count()):
+                if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
+                    self.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
+
+            self.ui.pref_apply_button.setStyleSheet("QPushButton {color: red;}")
 
-        self.preferences_changed_flag = True
+            self.preferences_changed_flag = True
 
     def on_tools_database(self):
         """
@@ -9908,9 +9918,34 @@ class App(QtCore.QObject):
             self.toggle_codeeditor = False
 
     def on_code_editor_close(self):
-        print("closed")
         self.toggle_codeeditor = False
 
+    def goto_text_line(self):
+        """
+        Will scroll a text to the specified text line.
+
+        :return: None
+        """
+        dia_box = Dialog_box(title=_("Go to Line ..."),
+                             label=_("Line:"),
+                             icon=QtGui.QIcon(self.resource_location + '/jump_to16.png'),
+                             initial_text='')
+        try:
+            line = int(dia_box.location) - 1
+        except (ValueError, TypeError):
+            line = 0
+
+        if dia_box.ok:
+            # make sure to move first the cursor at the end so after finding the line the line will be positioned
+            # at the top of the window
+            self.ui.plot_tab_area.currentWidget().code_editor.moveCursor(QTextCursor.End)
+            # get the document() of the TextEditor
+            doc = self.ui.plot_tab_area.currentWidget().code_editor.document()
+            # create a Text Cursor based on the searched line
+            cursor = QTextCursor(doc.findBlockByLineNumber(line))
+            # set cursor of the code editor with the cursor at the searcehd line
+            self.ui.plot_tab_area.currentWidget().code_editor.setTextCursor(cursor)
+
     def on_filenewscript(self, silent=False, name=None, text=None):
         """
         Will create a new script file and open it in the Code Editor
@@ -9921,8 +9956,7 @@ class App(QtCore.QObject):
         :return: None
         """
         if silent is False:
-            self.inform.emit('[success] %s' %
-                             _("New TCL script file created in Code Editor."))
+            self.inform.emit('[success] %s' % _("New TCL script file created in Code Editor."))
 
         # delete the absolute and relative position and messages in the infobar
         self.ui.position_label.setText("")
@@ -10025,8 +10059,7 @@ class App(QtCore.QObject):
                         self.shell._sysShell.exec_command(cmd_line_shellfile_content, no_echo=True)
 
                 if silent is False:
-                    self.inform.emit('[success] %s' %
-                                     _("TCL script file opened in Code Editor and executed."))
+                    self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
             except Exception as e:
                 log.debug("App.on_filerunscript() -> %s" % str(e))
                 sys.exit(2)

+ 122 - 115
FlatCAMObj.py

@@ -662,6 +662,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         self.fill_color = self.app.defaults['gerber_plot_fill']
         self.outline_color = self.app.defaults['gerber_plot_line']
 
+        # keep track if the UI is built so we don't have to build it every time
+        self.ui_build = False
+
+        # build only once the aperture storage (takes time)
+        self.build_aperture_storage = False
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -834,121 +840,118 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     def build_ui(self):
         FlatCAMObj.build_ui(self)
 
-        try:
-            # if connected, disconnect the signal from the slot on item_changed as it creates issues
-            self.ui.apertures_table.itemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        self.apertures_row = 0
-        aper_no = self.apertures_row + 1
-        sort = []
-        for k, v in list(self.apertures.items()):
-            sort.append(int(k))
-        sorted_apertures = sorted(sort)
-
-        # sort = []
-        # for k, v in list(self.aperture_macros.items()):
-        #     sort.append(k)
-        # sorted_macros = sorted(sort)
-
-        # n = len(sorted_apertures) + len(sorted_macros)
-        n = len(sorted_apertures)
-        self.ui.apertures_table.setRowCount(n)
-
-        for ap_code in sorted_apertures:
-            ap_code = str(ap_code)
-
-            ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
-            ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
-
-            ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
-            ap_code_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            ap_type_item = QtWidgets.QTableWidgetItem(str(self.apertures[ap_code]['type']))
-            ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
-                ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['width'],
-                                    self.decimals, self.apertures[ap_code]['height']
-                                    )
-                )
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            elif str(self.apertures[ap_code]['type']) == 'P':
-                ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['diam'],
-                                    self.decimals, self.apertures[ap_code]['nVertices'])
-                )
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            else:
-                ap_dim_item = QtWidgets.QTableWidgetItem('')
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
+        if self.ui.aperture_table_visibility_cb.get_value() and self.ui_build is False:
+            self.ui_build = True
 
             try:
-                if self.apertures[ap_code]['size'] is not None:
-                    ap_size_item = QtWidgets.QTableWidgetItem(
-                        '%.*f' % (self.decimals, float(self.apertures[ap_code]['size'])))
-                else:
-                    ap_size_item = QtWidgets.QTableWidgetItem('')
-            except KeyError:
-                ap_size_item = QtWidgets.QTableWidgetItem('')
-            ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            mark_item = FCCheckBox()
-            mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            # if self.ui.aperture_table_visibility_cb.isChecked():
-            #     mark_item.setChecked(True)
+                # if connected, disconnect the signal from the slot on item_changed as it creates issues
+                self.ui.apertures_table.itemChanged.disconnect()
+            except (TypeError, AttributeError):
+                pass
 
-            self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
-            self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
-            self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item)   # Aperture Dimensions
-            self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item)   # Aperture Dimensions
+            self.apertures_row = 0
+            aper_no = self.apertures_row + 1
+            sort = []
+            for k, v in list(self.apertures.items()):
+                sort.append(int(k))
+            sorted_apertures = sorted(sort)
 
-            empty_plot_item = QtWidgets.QTableWidgetItem('')
-            empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.apertures_table.setItem(self.apertures_row, 5, empty_plot_item)
-            self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
+            n = len(sorted_apertures)
+            self.ui.apertures_table.setRowCount(n)
 
-            self.apertures_row += 1
+            for ap_code in sorted_apertures:
+                ap_code = str(ap_code)
 
-        self.ui.apertures_table.selectColumn(0)
-        self.ui.apertures_table.resizeColumnsToContents()
-        self.ui.apertures_table.resizeRowsToContents()
+                ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
+                ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+                self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
 
-        vertical_header = self.ui.apertures_table.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+                ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
+                ap_code_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        horizontal_header = self.ui.apertures_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 27)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(4,  QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(5, 17)
-        self.ui.apertures_table.setColumnWidth(5, 17)
+                ap_type_item = QtWidgets.QTableWidgetItem(str(self.apertures[ap_code]['type']))
+                ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-        self.ui.apertures_table.setSortingEnabled(False)
-        self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
-        self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
+                if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
+                    ap_dim_item = QtWidgets.QTableWidgetItem(
+                        '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['width'],
+                                        self.decimals, self.apertures[ap_code]['height']
+                                        )
+                    )
+                    ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
+                elif str(self.apertures[ap_code]['type']) == 'P':
+                    ap_dim_item = QtWidgets.QTableWidgetItem(
+                        '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['diam'],
+                                        self.decimals, self.apertures[ap_code]['nVertices'])
+                    )
+                    ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
+                else:
+                    ap_dim_item = QtWidgets.QTableWidgetItem('')
+                    ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
-        if self.marked_rows:
-            for row in range(self.ui.apertures_table.rowCount()):
                 try:
-                    self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
-                except IndexError:
-                    pass
+                    if self.apertures[ap_code]['size'] is not None:
+                        ap_size_item = QtWidgets.QTableWidgetItem(
+                            '%.*f' % (self.decimals, float(self.apertures[ap_code]['size'])))
+                    else:
+                        ap_size_item = QtWidgets.QTableWidgetItem('')
+                except KeyError:
+                    ap_size_item = QtWidgets.QTableWidgetItem('')
+                ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled)
+
+                mark_item = FCCheckBox()
+                mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
+                # if self.ui.aperture_table_visibility_cb.isChecked():
+                #     mark_item.setChecked(True)
+
+                self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
+                self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
+                self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item)   # Aperture Dimensions
+                self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item)   # Aperture Dimensions
+
+                empty_plot_item = QtWidgets.QTableWidgetItem('')
+                empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+                self.ui.apertures_table.setItem(self.apertures_row, 5, empty_plot_item)
+                self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
+
+                self.apertures_row += 1
+
+            self.ui.apertures_table.selectColumn(0)
+            self.ui.apertures_table.resizeColumnsToContents()
+            self.ui.apertures_table.resizeRowsToContents()
+
+            vertical_header = self.ui.apertures_table.verticalHeader()
+            # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+            vertical_header.hide()
+            self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+            horizontal_header = self.ui.apertures_table.horizontalHeader()
+            horizontal_header.setMinimumSectionSize(10)
+            horizontal_header.setDefaultSectionSize(70)
+            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+            horizontal_header.resizeSection(0, 27)
+            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+            horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+            horizontal_header.setSectionResizeMode(4,  QtWidgets.QHeaderView.Stretch)
+            horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed)
+            horizontal_header.resizeSection(5, 17)
+            self.ui.apertures_table.setColumnWidth(5, 17)
+
+            self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+            self.ui.apertures_table.setSortingEnabled(False)
+            self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
+            self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
+
+            # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
+            if self.marked_rows:
+                for row in range(self.ui.apertures_table.rowCount()):
+                    try:
+                        self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
+                    except IndexError:
+                        pass
 
-        self.ui_connect()
+            self.ui_connect()
 
     def ui_connect(self):
         for row in range(self.ui.apertures_table.rowCount()):
@@ -1615,13 +1618,16 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     def on_aperture_table_visibility_change(self):
         if self.ui.aperture_table_visibility_cb.isChecked():
             # add the shapes storage for marking apertures
-            if self.app.is_legacy is False:
-                for ap_code in self.apertures:
-                    self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=1)
-            else:
-                for ap_code in self.apertures:
-                    self.mark_shapes[ap_code] = ShapeCollectionLegacy(obj=self, app=self.app,
-                                                                      name=self.options['name'] + str(ap_code))
+            if self.build_aperture_storage is False:
+                self.build_aperture_storage = True
+
+                if self.app.is_legacy is False:
+                    for ap_code in self.apertures:
+                        self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=1)
+                else:
+                    for ap_code in self.apertures:
+                        self.mark_shapes[ap_code] = ShapeCollectionLegacy(obj=self, app=self.app,
+                                                                          name=self.options['name'] + str(ap_code))
 
             self.ui.apertures_table.setVisible(True)
             for ap in self.mark_shapes:
@@ -1629,6 +1635,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             self.ui.mark_all_cb.setVisible(True)
             self.ui.mark_all_cb.setChecked(False)
+            self.build_ui()
         else:
             self.ui.apertures_table.setVisible(False)
 
@@ -1640,9 +1647,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     self.ui.apertures_table.cellWidget(row, 5).set_value(False)
                 self.clear_plot_apertures()
 
-                for ap in list(self.mark_shapes.keys()):
-                    # self.mark_shapes[ap].enabled = False
-                    del self.mark_shapes[ap]
+                # for ap in list(self.mark_shapes.keys()):
+                #     # self.mark_shapes[ap].enabled = False
+                #     del self.mark_shapes[ap]
             except Exception as e:
                 log.debug(" FlatCAMGerber.on_aperture_visibility_changed() --> %s" % str(e))
 
@@ -2208,8 +2215,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
         self.replotApertures.emit()
 
-    def buffer(self, distance, join):
-        Gerber.buffer(self, distance=distance, join=join)
+    def buffer(self, distance, join, factor=None):
+        Gerber.buffer(self, distance=distance, join=join, factor=factor)
         self.replotApertures.emit()
 
     def serialize(self):

+ 17 - 0
README.md

@@ -9,6 +9,23 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+30.12.2019
+
+- Buffer sub-tool in Transform Tool: added the possibility to apply a factor effectively scaling the aperture size thus the copper features sizes
+- in Transform Tool adjusted the GUI
+- fixed some decimals issues in NCC Tool, Paint Tool and Excellon Editor (they were still using the harcoded values)
+- some small updates in the NCC Tool
+
+29.12.2019
+
+- the Apply button text in Preferences is now made red when changes were made and require to be applied
+- the Gerber UI is built only once now so the process is lighter on CPU
+- the Gerber apertures marking shapes storage is now built only once because the more are built the more sluggish is the interface
+- added a new function called by shortcut key combo CTRL+G when the current widget in Plot Area is an Code Editor. It will jump to the specified line in the text.
+- fixed a small where the app tried to hide a label that I've removed previously
+- in Paint Tool Preferences allowed to add a list of initial tools separated by comma
+- in Geometry Paint Tool fixed the Overlap rate to work between 0 and 99.9999%
+
 28.12.2019
 
 - more updates to the Preferences window and in some other parts of the GUI

+ 17 - 10
camlib.py

@@ -2118,11 +2118,11 @@ class Geometry(object):
         #     self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
         #                                         origin=(px, py))
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor):
         """
 
-        :param distance:
-        :param join:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
 
@@ -2145,7 +2145,10 @@ class Geometry(object):
                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
                         self.old_disp_number = disp_number
 
-                    return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
+                    if factor is None:
+                        return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
+                    else:
+                        return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
                 except AttributeError:
                     return obj
 
@@ -2155,20 +2158,23 @@ class Geometry(object):
                     # variables to display the percentage of work done
                     self.geo_len = 0
                     try:
-                        for __ in self.tools[tool]['solid_geometry']:
-                            self.geo_len += 1
+                        self.geo_len += len(self.tools[tool]['solid_geometry'])
                     except TypeError:
-                        self.geo_len = 1
+                        self.geo_len += 1
                     self.old_disp_number = 0
                     self.el_count = 0
 
-                    self.tools[tool]['solid_geometry'] = buffer_geom(self.tools[tool]['solid_geometry'])
+                    res = buffer_geom(self.tools[tool]['solid_geometry'])
+                    try:
+                        __ = iter(res)
+                        self.tools[tool]['solid_geometry'] = res
+                    except TypeError:
+                        self.tools[tool]['solid_geometry'] = [res]
 
             # 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
             self.old_disp_number = 0
@@ -2182,6 +2188,7 @@ class Geometry(object):
 
         self.app.proc_container.new_text = ''
 
+
 class AttrDict(dict):
     def __init__(self, *args, **kwargs):
         super(AttrDict, self).__init__(*args, **kwargs)

+ 0 - 5
flatcamEditors/FlatCAMExcEditor.py

@@ -2227,11 +2227,6 @@ class FlatCAMExcEditor(QtCore.QObject):
         # updated units
         self.units = self.app.defaults['units'].upper()
 
-        if self.units == "IN":
-            self.decimals = 4
-        else:
-            self.decimals = 2
-
         self.olddia_newdia.clear()
         self.tool2tooldia.clear()
 

+ 8 - 10
flatcamEditors/FlatCAMGeoEditor.py

@@ -441,8 +441,7 @@ class PaintOptionsTool(FlatCAMTool):
         # Tool dia
         ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         ptdlabel.setToolTip(
-           _("Diameter of the tool to\n"
-             "be used in the operation.")
+           _("Diameter of the tool to be used in the operation.")
         )
         grid.addWidget(ptdlabel, 0, 0)
 
@@ -463,10 +462,10 @@ class PaintOptionsTool(FlatCAMTool):
               "due of too many paths.")
         )
         self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
-        self.paintoverlap_entry.set_range(0.0000, 1.0000)
+        self.paintoverlap_entry.set_range(0.0000, 99.9999)
         self.paintoverlap_entry.set_precision(self.decimals)
         self.paintoverlap_entry.setWrapping(True)
-        self.paintoverlap_entry.setSingleStep(0.1)
+        self.paintoverlap_entry.setSingleStep(1)
 
         grid.addWidget(ovlabel, 1, 0)
         grid.addWidget(self.paintoverlap_entry, 1, 1)
@@ -525,7 +524,6 @@ class PaintOptionsTool(FlatCAMTool):
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
-        hlay.addStretch()
         self.paint_button = QtWidgets.QPushButton(_("Paint"))
         hlay.addWidget(self.paint_button)
 
@@ -584,9 +582,9 @@ class PaintOptionsTool(FlatCAMTool):
                                  _("Paint cancelled. No shape selected."))
             return
 
-        tooldia = float(self.painttooldia_entry.get_value())
-        overlap = float(self.paintoverlap_entry.get_value())
-        margin = float(self.paintmargin_entry.get_value())
+        tooldia = self.painttooldia_entry.get_value()
+        overlap = self.paintoverlap_entry.get_value() / 100.0
+        margin = self.paintmargin_entry.get_value()
 
         method = self.paintmethod_combo.get_value()
         contour = self.paintcontour_cb.get_value()
@@ -4749,9 +4747,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
     def paint(self, tooldia, overlap, margin, connect, contour, method):
 
-        if overlap >= 1:
+        if overlap >= 100:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Could not do Paint. Overlap value has to be less than 1.00 (100%%)."))
+                                 _("Could not do Paint. Overlap value has to be less than 100%%."))
             return
 
         self.paint_tooldia = tooldia

+ 5 - 1
flatcamGUI/FlatCAMGUI.py

@@ -2723,7 +2723,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
                 # Open Gerber file
                 if key == QtCore.Qt.Key_G:
-                    self.app.on_fileopengerber()
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if 'editor' in widget_name.lower():
+                        self.app.goto_text_line()
+                    else:
+                        self.app.on_fileopengerber()
 
                 # Distance Tool
                 if key == QtCore.Qt.Key_M:

+ 30 - 8
flatcamGUI/PreferencesUI.py

@@ -1729,7 +1729,6 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.layout.addStretch()
 
         if sys.platform != 'win32':
-            self.portability_label.hide()
             self.portability_cb.hide()
 
         # splash screen button signal
@@ -2521,7 +2520,9 @@ class GerberEditorPrefGroupUI(OptionsGroupUI):
 
         self.adddim_label = QtWidgets.QLabel('%s:' % _('Aperture Dimensions'))
         self.adddim_label.setToolTip(
-            _("Diameters of the cutting tools, separated by ','")
+            _("Diameters of the cutting tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
         )
         grid0.addWidget(self.adddim_label, 5, 0)
         self.adddim_entry = FCEntry()
@@ -3874,7 +3875,9 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
         # Tooldia
         tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         tdlabel.setToolTip(
-            _("Diameters of the cutting tools, separated by ','")
+            _("Diameters of the cutting tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
         )
         self.cnctooldia_entry = FCEntry()
 
@@ -5026,7 +5029,9 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
 
         ncctdlabel = QtWidgets.QLabel('%s:' % _('Tools dia'))
         ncctdlabel.setToolTip(
-            _("Diameters of the cutting tools, separated by ','")
+            _("Diameters of the cutting tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
         )
         grid0.addWidget(ncctdlabel, 0, 0)
         self.ncc_tool_dia_entry = FCEntry()
@@ -5534,8 +5539,9 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         # Tool dia
         ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         ptdlabel.setToolTip(
-            _("Diameter of the tool to\n"
-              "be used in the operation.")
+            _("Diameters of the cutting tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
         )
         grid0.addWidget(ptdlabel, 0, 0)
 
@@ -6410,6 +6416,23 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.buffer_label, 17, 0)
         grid0.addWidget(self.buffer_entry, 17, 1)
 
+        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased by the 'factor'.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        grid0.addWidget(self.buffer_factor_label, 18, 0)
+        grid0.addWidget(self.buffer_factor_entry, 18, 1)
+
         self.buffer_rounded_cb = FCCheckBox()
         self.buffer_rounded_cb.setText('%s' % _("Rounded"))
         self.buffer_rounded_cb.setToolTip(
@@ -6419,9 +6442,8 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
               "of the buffered shape.")
         )
 
-        grid0.addWidget(self.buffer_rounded_cb, 18, 0, 1, 2)
+        grid0.addWidget(self.buffer_rounded_cb, 19, 0, 1, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 19, 0, 1, 2)
 
         self.layout.addStretch()
 

+ 17 - 6
flatcamParsers/ParseExcellon.py

@@ -1459,11 +1459,11 @@ class Excellon(Geometry):
         self.create_geometry()
         self.app.proc_container.new_text = ''
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor):
         """
 
-        :param distance:
-        :param join:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
         log.debug("flatcamParsers.ParseExcellon.Excellon.buffer()")
@@ -1479,13 +1479,24 @@ class Excellon(Geometry):
                 return new_obj
             else:
                 try:
-                    return obj.buffer(distance, resolution=self.geo_steps_per_circle)
+                    if factor is None:
+                        return obj.buffer(distance, resolution=self.geo_steps_per_circle)
+                    else:
+                        return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
                 except AttributeError:
                     return obj
 
         # buffer solid_geometry
         for tool, tool_dict in list(self.tools.items()):
-            self.tools[tool]['solid_geometry'] = buffer_geom(tool_dict['solid_geometry'])
-            self.tools[tool]['C'] += distance
+            res = buffer_geom(tool_dict['solid_geometry'])
+            try:
+                __ = iter(res)
+                self.tools[tool]['solid_geometry'] = res
+            except TypeError:
+                self.tools[tool]['solid_geometry'] = [res]
+            if factor is None:
+                self.tools[tool]['C'] += distance
+            else:
+                self.tools[tool]['C'] *= distance
 
         self.create_geometry()

+ 130 - 56
flatcamParsers/ParseGerber.py

@@ -2188,14 +2188,14 @@ class Gerber(Geometry):
         except Exception as e:
             log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
             return 'fail'
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Rotate done."))
+        self.app.inform.emit('[success] %s' % _("Gerber Rotate done."))
         self.app.proc_container.new_text = ''
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor=None):
         """
 
-        :param distance:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
         log.debug("parseGerber.Gerber.buffer()")
@@ -2206,69 +2206,143 @@ 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
-        except TypeError:
+            self.geo_len = len(self.solid_geometry)
+        except (TypeError, ValueError):
             self.geo_len = 1
 
         self.old_disp_number = 0
         self.el_count = 0
 
-        def buffer_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(buffer_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
+        if factor is None:
+            def buffer_geom(obj):
+                if type(obj) is list:
+                    new_obj = []
+                    for g in obj:
+                        new_obj.append(buffer_geom(g))
+                    return new_obj
+                else:
+                    try:
+                        self.el_count += 1
+                        disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                        if self.old_disp_number < disp_number <= 100:
+                            self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                            self.old_disp_number = disp_number
 
-                    return obj.buffer(distance, resolution=self.steps_per_circle, join_style=join)
-                except AttributeError:
-                    return obj
+                        return obj.buffer(distance, resolution=int(self.steps_per_circle), join_style=join)
 
-        self.solid_geometry = buffer_geom(self.solid_geometry)
+                    except AttributeError:
+                        return obj
 
-        # we need to buffer the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                new_geometry = list()
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        new_geo_el = dict()
-                        if 'solid' in geo_el:
-                            new_geo_el['solid'] = buffer_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            new_geo_el['follow'] = buffer_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            new_geo_el['clear'] = buffer_geom(geo_el['clear'])
-                        new_geometry.append(new_geo_el)
+            res = buffer_geom(self.solid_geometry)
+            try:
+                __ = iter(res)
+                self.solid_geometry = res
+            except TypeError:
+                self.solid_geometry = [res]
 
-                self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+            # we need to buffer the geometry stored in the Gerber apertures, too
+            try:
+                for apid in self.apertures:
+                    new_geometry = list()
+                    if 'geometry' in self.apertures[apid]:
+                        for geo_el in self.apertures[apid]['geometry']:
+                            new_geo_el = dict()
+                            if 'solid' in geo_el:
+                                new_geo_el['solid'] = buffer_geom(geo_el['solid'])
+                            if 'follow' in geo_el:
+                                new_geo_el['follow'] = geo_el['follow']
+                            if 'clear' in geo_el:
+                                new_geo_el['clear'] = buffer_geom(geo_el['clear'])
+                            new_geometry.append(new_geo_el)
+
+                    self.apertures[apid]['geometry'] = deepcopy(new_geometry)
 
-                try:
-                    if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
-                        self.apertures[apid]['width'] += (distance * 2)
-                        self.apertures[apid]['height'] += (distance * 2)
-                    elif str(self.apertures[apid]['type']) == 'P':
-                        self.apertures[apid]['diam'] += (distance * 2)
-                        self.apertures[apid]['nVertices'] += (distance * 2)
-                except KeyError:
-                    pass
+                    try:
+                        if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                            self.apertures[apid]['width'] += (distance * 2)
+                            self.apertures[apid]['height'] += (distance * 2)
+                        elif str(self.apertures[apid]['type']) == 'P':
+                            self.apertures[apid]['diam'] += (distance * 2)
+                            self.apertures[apid]['nVertices'] += (distance * 2)
+                    except KeyError:
+                        pass
 
-                try:
-                    if self.apertures[apid]['size'] is not None:
-                        self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2))
-                except KeyError:
-                    pass
-        except Exception as e:
-            log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
-            return 'fail'
+                    try:
+                        if self.apertures[apid]['size'] is not None:
+                            self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2))
+                    except KeyError:
+                        pass
+            except Exception as e:
+                log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
+                return 'fail'
+        else:
+            try:
+                for apid in self.apertures:
+                    try:
+                        if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                            self.apertures[apid]['width'] *= distance
+                            self.apertures[apid]['height'] *= distance
+                        elif str(self.apertures[apid]['type']) == 'P':
+                            self.apertures[apid]['diam'] *= distance
+                            self.apertures[apid]['nVertices'] *= distance
+                    except KeyError:
+                        pass
+
+                    try:
+                        if self.apertures[apid]['size'] is not None:
+                            self.apertures[apid]['size'] = float(self.apertures[apid]['size']) * distance
+                    except KeyError:
+                        pass
+
+                    new_geometry = list()
+                    if 'geometry' in self.apertures[apid]:
+                        for geo_el in self.apertures[apid]['geometry']:
+                            new_geo_el = dict()
+                            if 'follow' in geo_el:
+                                new_geo_el['follow'] = geo_el['follow']
+                                size = float(self.apertures[apid]['size'])
+                                if isinstance(new_geo_el['follow'], Point):
+                                    if str(self.apertures[apid]['type']) == 'C':
+                                        new_geo_el['solid'] = geo_el['follow'].buffer(
+                                            size / 1.9999,
+                                            resolution=int(self.steps_per_circle)
+                                        )
+                                    elif str(self.apertures[apid]['type']) == 'R':
+                                        width = self.apertures[apid]['width']
+                                        height = self.apertures[apid]['height']
+                                        minx = new_geo_el['follow'].x - width / 2
+                                        maxx = new_geo_el['follow'].x + width / 2
+                                        miny = new_geo_el['follow'].y - height / 2
+                                        maxy = new_geo_el['follow'].y + height / 2
+
+                                        geo_p = shply_box(minx, miny, maxx, maxy)
+                                        new_geo_el['solid'] = geo_p
+                                    else:
+                                        log.debug("flatcamParsers.ParseGerber.Gerber.buffer() --> "
+                                                  "ap type not supported")
+                                else:
+                                    new_geo_el['solid'] = geo_el['follow'].buffer(
+                                        size/1.9999,
+                                        resolution=int(self.steps_per_circle)
+                                    )
+                            if 'clear' in geo_el:
+                                new_geo_el['clear'] = geo_el['clear']
+                            new_geometry.append(new_geo_el)
+
+                    self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+            except Exception as e:
+                log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
+                return 'fail'
+
+            # make the new solid_geometry
+            new_solid_geo = list()
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    new_solid_geo += [geo_el['solid'] for geo_el in self.apertures[apid]['geometry']]
+
+            self.solid_geometry = MultiPolygon(new_solid_geo)
+            self.solid_geometry = self.solid_geometry.buffer(0.000001)
+            self.solid_geometry = self.solid_geometry.buffer(-0.000001)
 
         self.app.inform.emit('[success] %s' % _("Gerber Buffer done."))
         self.app.proc_container.new_text = ''

+ 5 - 8
flatcamTools/ToolNonCopperClear.py

@@ -634,11 +634,6 @@ class NonCopperClear(FlatCAMTool, Gerber):
     def set_tool_ui(self):
         self.units = self.app.defaults['units'].upper()
 
-        if self.units == "IN":
-            self.decimals = 4
-        else:
-            self.decimals = 2
-
         self.tools_frame.show()
 
         self.ncc_order_radio.set_value(self.app.defaults["tools_nccorder"])
@@ -701,10 +696,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
         })
 
         try:
+            dias = [float(self.app.defaults["tools_ncctools"])]
+        except (ValueError, TypeError):
             dias = [float(eval(dia)) for dia in self.app.defaults["tools_ncctools"].split(",") if dia != '']
-        except Exception as e:
-            log.error("At least one tool diameter needed. "
-                      "Verify in Edit -> Preferences -> TOOLS -> NCC Tools. %s" % str(e))
+
+        if not dias:
+            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> TOOLS -> NCC Tools.")
             return
 
         self.tooluid = 0

+ 16 - 9
flatcamTools/ToolPaint.py

@@ -601,13 +601,6 @@ class ToolPaint(FlatCAMTool, Gerber):
         # updated units
         self.units = self.app.defaults['units'].upper()
 
-        if self.units == "IN":
-            self.decimals = 4
-            self.addtool_entry.set_value(0.039)
-        else:
-            self.decimals = 2
-            self.addtool_entry.set_value(1)
-
         # set the working variables to a known state
         self.paint_tools.clear()
         self.tooluid = 0
@@ -637,7 +630,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
 
-            "tooldia": float(self.app.defaults["tools_painttooldia"]),
+            "tooldia": self.app.defaults["tools_painttooldia"],
             "paintmargin": float(self.app.defaults["tools_paintmargin"]),
             "paintmethod": self.app.defaults["tools_paintmethod"],
             "selectmethod": self.app.defaults["tools_selectmethod"],
@@ -646,9 +639,23 @@ class ToolPaint(FlatCAMTool, Gerber):
             "paintoverlap": self.app.defaults["tools_paintoverlap"]
         })
 
+        try:
+            diameters = [float(self.app.defaults["tools_painttooldia"])]
+        except (ValueError, TypeError):
+            diameters = [eval(x) for x in self.app.defaults["tools_painttooldia"].split(",") if x != '']
+
+        if not diameters:
+            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> TOOLS -> NCC Tools.")
+            self.build_ui()
+            # if the Paint Method is "Single" disable the tool table context menu
+            if self.default_data["selectmethod"] == "single":
+                self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
+            return
+
         # call on self.on_tool_add() counts as an call to self.build_ui()
         # through this, we add a initial row / tool in the tool_table
-        self.on_tool_add(float(self.app.defaults["tools_painttooldia"]), muted=True)
+        for dia in diameters:
+            self.on_tool_add(dia, muted=True)
 
         # if the Paint Method is "Single" disable the tool table context menu
         if self.default_data["selectmethod"] == "single":

+ 0 - 5
flatcamTools/ToolSolderPaste.py

@@ -621,11 +621,6 @@ class SolderPaste(FlatCAMTool):
 
         self.units = self.app.defaults['units'].upper()
 
-        if self.units == "IN":
-            self.decimals = 4
-        else:
-            self.decimals = 2
-
         for name in list(self.app.preprocessors.keys()):
             # populate only with preprocessor files that start with 'Paste_'
             if name.partition('_')[0] != 'Paste':

+ 76 - 22
flatcamTools/ToolTransform.py

@@ -89,7 +89,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.rotate_entry, 1, 1)
         grid0.addWidget(self.rotate_button, 1, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 2, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 3)
 
         # ## Skew Title
         skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
@@ -139,7 +142,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.skewy_entry, 5, 1)
         grid0.addWidget(self.skewy_button, 5, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 6, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 3)
 
         # ## Scale Title
         scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
@@ -208,7 +214,11 @@ class ToolTransform(FlatCAMTool):
 
         grid0.addWidget(self.scale_link_cb, 10, 0)
         grid0.addWidget(self.scale_zero_ref_cb, 10, 1)
-        grid0.addWidget(QtWidgets.QLabel(''), 11, 0)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 11, 0, 1, 3)
 
         # ## Offset Title
         offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
@@ -256,7 +266,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.offy_entry, 14, 1)
         grid0.addWidget(self.offy_button, 14, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 15, 0, 1, 3)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 15, 0, 1, 3)
 
         # ## Flip Title
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
@@ -323,7 +336,10 @@ class ToolTransform(FlatCAMTool):
 
         grid0.addWidget(self.flip_ref_button, 20, 0, 1, 3)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 21, 0, 1, 3)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 21, 0, 1, 3)
 
         # ## Buffer Title
         buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
@@ -343,13 +359,11 @@ class ToolTransform(FlatCAMTool):
         self.buffer_entry.setWrapping(True)
         self.buffer_entry.set_range(-9999.9999, 9999.9999)
 
-        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-
         self.buffer_button = FCButton()
-        self.buffer_button.set_value(_("Buffer"))
+        self.buffer_button.set_value(_("Buffer D"))
         self.buffer_button.setToolTip(
             _("Create the buffer effect on each geometry,\n"
-              "element from the selected object.")
+              "element from the selected object, using the distance.")
         )
         self.buffer_button.setMinimumWidth(90)
 
@@ -357,8 +371,33 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.buffer_entry, 23, 1)
         grid0.addWidget(self.buffer_button, 23, 2)
 
-        self.buffer_rounded_cb = FCCheckBox()
-        self.buffer_rounded_cb.setText('%s' % _("Rounded"))
+        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased by the 'factor'.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        self.buffer_factor_button = FCButton()
+        self.buffer_factor_button.set_value(_("Buffer F"))
+        self.buffer_factor_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the factor.")
+        )
+        self.buffer_factor_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.buffer_factor_label, 24, 0)
+        grid0.addWidget(self.buffer_factor_entry, 24, 1)
+        grid0.addWidget(self.buffer_factor_button, 24, 2)
+
+        self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
         self.buffer_rounded_cb.setToolTip(
             _("If checked then the buffer will surround the buffered shape,\n"
               "every corner will be rounded.\n"
@@ -366,9 +405,9 @@ class ToolTransform(FlatCAMTool):
               "of the buffered shape.")
         )
 
-        grid0.addWidget(self.buffer_rounded_cb, 24, 0, 1, 3)
+        grid0.addWidget(self.buffer_rounded_cb, 25, 0, 1, 3)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 25, 0, 1, 3)
+        grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3)
 
         self.transform_lay.addStretch()
 
@@ -383,7 +422,8 @@ class ToolTransform(FlatCAMTool):
         self.flipx_button.clicked.connect(self.on_flipx)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
-        self.buffer_button.clicked.connect(self.on_buffer)
+        self.buffer_button.clicked.connect(self.on_buffer_by_distance)
+        self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
 
         # self.rotate_entry.returnPressed.connect(self.on_rotate)
         # self.skewx_entry.returnPressed.connect(self.on_skewx)
@@ -392,7 +432,7 @@ class ToolTransform(FlatCAMTool):
         # self.scaley_entry.returnPressed.connect(self.on_scaley)
         # self.offx_entry.returnPressed.connect(self.on_offx)
         # self.offy_entry.returnPressed.connect(self.on_offy)
-        # self.buffer_entry.returnPressed.connect(self.on_buffer)
+        # self.buffer_entry.returnPressed.connect(self.on_buffer_by_distance)
 
     def run(self, toggle=True):
         self.app.report_usage("ToolTransform()")
@@ -486,6 +526,11 @@ class ToolTransform(FlatCAMTool):
         else:
             self.buffer_entry.set_value(0.0)
 
+        if self.app.defaults["tools_transform_buffer_factor"]:
+            self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
+        else:
+            self.buffer_factor_entry.set_value(100.0)
+
         if self.app.defaults["tools_transform_buffer_corner"]:
             self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
         else:
@@ -589,13 +634,23 @@ class ToolTransform(FlatCAMTool):
         self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
         return
 
-    def on_buffer(self):
+    def on_buffer_by_distance(self):
         value = self.buffer_entry.get_value()
         join = 1 if self.buffer_rounded_cb.get_value() else 2
 
         self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
         return
 
+    def on_buffer_by_factor(self):
+        value = self.buffer_factor_entry.get_value() / 100.0
+        join = 1 if self.buffer_rounded_cb.get_value() else 2
+
+        # tell the buffer method to use the factor
+        factor = True
+
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
+        return
+
     def on_rotate_action(self, num):
         obj_list = self.app.collection.get_selected()
         xminlist = []
@@ -604,8 +659,7 @@ class ToolTransform(FlatCAMTool):
         ymaxlist = []
 
         if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("No object selected. Please Select an object to rotate!"))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to rotate!"))
             return
         else:
             with self.app.proc_container.new(_("Appying Rotate")):
@@ -874,7 +928,7 @@ class ToolTransform(FlatCAMTool):
                                          (_("Due of"), str(e),  _("action was not executed.")))
                     return
 
-    def on_buffer_action(self, value, join):
+    def on_buffer_action(self, value, join, factor=None):
         obj_list = self.app.collection.get_selected()
 
         if not obj_list:
@@ -887,17 +941,17 @@ class ToolTransform(FlatCAMTool):
                         if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be buffered."))
                         elif sel_obj.kind.lower() == 'gerber':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
                             sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
                                                                          filename=None, local_use=sel_obj,
                                                                          use_thread=False)
                         elif sel_obj.kind.lower() == 'excellon':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
                             sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
                                                                            filename=None, local_use=sel_obj,
                                                                            use_thread=False)
                         elif sel_obj.kind.lower() == 'geometry':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
 
                         self.app.object_changed.emit(sel_obj)
                         sel_obj.plot()