Sfoglia il codice sorgente

- all tuple entries in the Preferences UI are now protected against letter entry
- all entries in the Preferences UI that have numerical entry are protected now against letters
- cleaned the Preferences UI in the Gerber area

Marius Stanciu 5 anni fa
parent
commit
c27a2d29e7

+ 9 - 6
AppGUI/GUIElements.py

@@ -570,10 +570,13 @@ class FCEntry3(FCEntry):
 
 
 class EvalEntry(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    def __init__(self, border_color=None, parent=None):
         super(EvalEntry, self).__init__(parent)
         self.readyToEdit = True
 
+        if border_color:
+            self.setStyleSheet("QLineEdit {border: 1px solid %s;}" % border_color)
+
         self.editingFinished.connect(self.on_edit_finished)
 
     def on_edit_finished(self):
@@ -639,7 +642,7 @@ class EvalEntry2(QtWidgets.QLineEdit):
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
-        evaled = 0.0
+
         try:
             evaled = eval(raw)
         except Exception as e:
@@ -660,8 +663,8 @@ class NumericalEvalEntry(EvalEntry):
     """
     Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
     """
-    def __init__(self):
-        super().__init__()
+    def __init__(self, border_color=None):
+        super().__init__(border_color=border_color)
 
         regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s]*")
         validator = QtGui.QRegExpValidator(regex, self)
@@ -672,8 +675,8 @@ class NumericalEvalTupleEntry(EvalEntry):
     """
     Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
     """
-    def __init__(self):
-        super().__init__()
+    def __init__(self, border_color=None):
+        super().__init__(border_color=border_color)
 
         regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s\,]*")
         validator = QtGui.QRegExpValidator(regex, self)

+ 28 - 265
AppGUI/ObjectUI.py

@@ -114,7 +114,7 @@ class ObjectUI(QtWidgets.QWidget):
             self.common_grid.addWidget(self.transform_label, 2, 0, 1, 2)
 
             # ### Scale ####
-            self.scale_entry = NumericalEvalEntry()
+            self.scale_entry = NumericalEvalEntry(border_color='#0069A9')
             self.scale_entry.set_value(1.0)
             self.scale_entry.setToolTip(
                 _("Factor by which to multiply\n"
@@ -132,7 +132,7 @@ class ObjectUI(QtWidgets.QWidget):
             self.common_grid.addWidget(self.scale_button, 3, 1)
 
             # ### Offset ####
-            self.offsetvector_entry = NumericalEvalTupleEntry()
+            self.offsetvector_entry = NumericalEvalTupleEntry(border_color='#0069A9')
             self.offsetvector_entry.setText("(0.0, 0.0)")
             self.offsetvector_entry.setToolTip(
                 _("Amount by which to move the object\n"
@@ -206,15 +206,17 @@ class GerberObjectUI(ObjectUI):
         grid0.addWidget(self.multicolored_cb, 0, 2)
 
         # Plot CB
-        self.plot_cb = FCCheckBox('%s' % _("Plot"))
-        # self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
-        self.plot_cb.setToolTip(_("Plot (show) this object."))
+        self.plot_lbl = FCLabel('%s' % _("Plot"))
+        self.plot_lbl.setToolTip(_("Plot (show) this object."))
+        self.plot_cb = FCCheckBox()
 
-        grid0.addWidget(self.plot_cb, 1, 0, 1, 2)
+        grid0.addWidget(self.plot_lbl, 1, 0)
+        grid0.addWidget(self.plot_cb, 1, 1)
 
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
+
         name_label = QtWidgets.QLabel("<b>%s:</b>" % _("Name"))
         self.name_entry = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
@@ -225,7 +227,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay_plot)
 
         # ### Gerber Apertures ####
-        self.apertures_table_label = QtWidgets.QLabel('<b>%s:</b>' % _('Apertures'))
+        self.apertures_table_label = QtWidgets.QLabel('%s:' % _('Apertures'))
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
         )
@@ -282,253 +284,21 @@ class GerberObjectUI(ObjectUI):
         # start with apertures table hidden
         self.apertures_table.setVisible(False)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.custom_box.addWidget(separator_line)
-
-        # Isolation Routing
-        self.isolation_routing_label = QtWidgets.QLabel("<b>%s</b>" % _("Isolation Routing"))
-        self.isolation_routing_label.setToolTip(
-            _("Create a Geometry object with\n"
-              "toolpaths to cut outside polygons.")
-        )
-        self.custom_box.addWidget(self.isolation_routing_label)
-
-        # ###########################################
-        # ########## NEW GRID #######################
-        # ###########################################
-
-        grid1 = QtWidgets.QGridLayout()
-        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'))
-        self.tool_type_label.setToolTip(
-            _("Choose which tool to use for Gerber isolation:\n"
-              "'Circular' or 'V-shape'.\n"
-              "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'}])
-
-        grid1.addWidget(self.tool_type_label, 0, 0)
-        grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2)
-
-        # Tip Dia
-        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
-        self.tipdialabel.setToolTip(
-            _("The tip diameter for V-Shape Tool")
-        )
-        self.tipdia_spinner = FCDoubleSpinner(callback=self.confirmation_message)
-        self.tipdia_spinner.set_range(-99.9999, 99.9999)
-        self.tipdia_spinner.set_precision(self.decimals)
-        self.tipdia_spinner.setSingleStep(0.1)
-        self.tipdia_spinner.setWrapping(True)
-        grid1.addWidget(self.tipdialabel, 1, 0)
-        grid1.addWidget(self.tipdia_spinner, 1, 1, 1, 2)
-
-        # Tip Angle
-        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
-        self.tipanglelabel.setToolTip(
-            _("The tip angle for V-Shape Tool.\n"
-              "In degree.")
-        )
-        self.tipangle_spinner = FCDoubleSpinner(callback=self.confirmation_message)
-        self.tipangle_spinner.set_range(1, 180)
-        self.tipangle_spinner.set_precision(self.decimals)
-        self.tipangle_spinner.setSingleStep(5)
-        self.tipangle_spinner.setWrapping(True)
-        grid1.addWidget(self.tipanglelabel, 2, 0)
-        grid1.addWidget(self.tipangle_spinner, 2, 1, 1, 2)
-
-        # Cut Z
-        self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
-        self.cutzlabel.setToolTip(
-            _("Cutting depth (negative)\n"
-              "below the copper surface.")
-        )
-        self.cutz_spinner = FCDoubleSpinner(callback=self.confirmation_message)
-        self.cutz_spinner.set_range(-9999.9999, 0.0000)
-        self.cutz_spinner.set_precision(self.decimals)
-        self.cutz_spinner.setSingleStep(0.1)
-        self.cutz_spinner.setWrapping(True)
-        grid1.addWidget(self.cutzlabel, 3, 0)
-        grid1.addWidget(self.cutz_spinner, 3, 1, 1, 2)
-
-        # Tool diameter
-        tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
-        tdlabel.setToolTip(
-            _("Diameter of the cutting tool.\n"
-              "If you want to have an isolation path\n"
-              "inside the actual shape of the Gerber\n"
-              "feature, use a negative value for\n"
-              "this parameter.")
-        )
-        tdlabel.setMinimumWidth(90)
-        self.iso_tool_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        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)
-
-        grid1.addWidget(tdlabel, 4, 0)
-        grid1.addWidget(self.iso_tool_dia_entry, 4, 1, 1, 2)
-
-        # Number of Passes
-        passlabel = QtWidgets.QLabel('%s:' % _('# Passes'))
-        passlabel.setToolTip(
-            _("Width of the isolation gap in\n"
-              "number (integer) of tool widths.")
-        )
-        passlabel.setMinimumWidth(90)
-        self.iso_width_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.iso_width_entry.set_range(1, 999)
-
-        grid1.addWidget(passlabel, 5, 0)
-        grid1.addWidget(self.iso_width_entry, 5, 1, 1, 2)
-
-        # Pass overlap
-        overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
-        overlabel.setToolTip(
-            _("How much (percentage) of the tool width to overlap each tool pass.")
-        )
-        overlabel.setMinimumWidth(90)
-        self.iso_overlap_entry = FCDoubleSpinner(suffix='%', callback=self.confirmation_message)
-        self.iso_overlap_entry.set_precision(self.decimals)
-        self.iso_overlap_entry.setWrapping(True)
-        self.iso_overlap_entry.set_range(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)
-
-        # Milling Type Radio Button
-        self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
-        self.milling_type_label.setToolTip(
-            _("Milling type:\n"
-              "- climb / best for precision milling and to reduce tool usage\n"
-              "- conventional / useful when there is no backlash compensation")
-        )
-        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'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'))
-        self.combine_passes_cb.setToolTip(
-            _("Combine all passes into one object")
-        )
-
         # generate follow
-        self.follow_cb = FCCheckBox(label=_('"Follow"'))
-        self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
-                                    "This means that it will cut through\n"
-                                    "the middle of the trace."))
-
-        # avoid an area from isolation
-        self.except_cb = FCCheckBox(label=_('Except'))
-        self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
-                                    "by checking this, the area of the object below\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
-        form_layout = QtWidgets.QFormLayout()
-        grid1.addLayout(form_layout, 9, 0, 1, 3)
-
-        # ################################################
-        # ##### Type of object to be excepted ############
-        # ################################################
-        self.type_obj_combo = FCComboBox()
-        self.type_obj_combo.addItems([_("Gerber"), _("Geometry")])
-
-        # we get rid of item1 ("Excellon") as it is not suitable
-        # self.type_obj_combo.view().setRowHidden(1, True)
-        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.resource_loc + "/flatcam_icon16.png"))
-        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.resource_loc + "/geometry16.png"))
-
-        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Obj Type"))
-        self.type_obj_combo_label.setToolTip(
-            _("Specify the type of object to be excepted from isolation.\n"
-              "It can be of type: Gerber or Geometry.\n"
-              "What is selected here will dictate the kind\n"
-              "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)
-
-        # ################################################
-        # ##### The object to be excepted ################
-        # ################################################
-        self.obj_combo = FCComboBox()
-
-        self.obj_label = QtWidgets.QLabel('%s:' % _("Object"))
-        self.obj_label.setToolTip(_("Object whose area will be removed from isolation geometry."))
-
-        form_layout.addRow(self.obj_label, self.obj_combo)
-
-        # ---------------------------------------------- #
-        # --------- 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"
-              "of the Gerber feature and inside means inside of\n"
-              "the Gerber feature, if possible at all. This means\n"
-              "that only if the Gerber feature has openings inside, they\n"
-              "will be isolated. If what is wanted is to cut isolation\n"
-              "inside the actual Gerber feature, use a negative tool\n"
-              "diameter above.")
-        )
-        grid1.addWidget(self.generate_iso_button, 12, 0, 1, 3)
-
+        self.follow_lbl = FCLabel('%s:' % _("Follow"))
+        self.follow_lbl.setToolTip(_("Generate a 'Follow' geometry.\n"
+                                     "This means that it will cut through\n"
+                                     "the middle of the trace."))
+        self.follow_lbl.setMinimumWidth(90)
+        self.follow_cb = FCCheckBox()
+
+        hf_lay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(hf_lay)
+        hf_lay.addWidget(self.follow_lbl)
+        hf_lay.addWidget(self.follow_cb)
+        hf_lay.addStretch()
+
+        # Buffer Geometry
         self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry'))
         self.create_buffer_button.setToolTip(
             _("This button is shown only when the Gerber file\n"
@@ -536,19 +306,12 @@ class GerberObjectUI(ObjectUI):
               "Clicking this will create the buffered geometry\n"
               "required for isolation.")
         )
-        grid1.addWidget(self.create_buffer_button, 13, 0, 1, 3)
-
-        self.ohis_iso = OptionalHideInputSection(
-            self.except_cb,
-            [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
-            logic=True
-        )
+        self.custom_box.addWidget(self.create_buffer_button)
 
-        separator_line2 = QtWidgets.QFrame()
-        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid1.addWidget(separator_line2, 14, 0, 1, 3)
-        # grid1.addWidget(QtWidgets.QLabel(''), 15, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.custom_box.addWidget(separator_line)
 
         # ###########################################
         # ########## NEW GRID #######################

+ 5 - 4
AppGUI/preferences/PreferencesUIManager.py

@@ -330,9 +330,9 @@ class PreferencesUIManager:
             "tools_iso_tooldia":        self.ui.tools_defaults_form.tools_iso_group.tool_dia_entry,
             "tools_iso_order":          self.ui.tools_defaults_form.tools_iso_group.order_radio,
             "tools_iso_tool_type":      self.ui.tools_defaults_form.tools_iso_group.tool_type_radio,
-            "tools_iso_tool_vtipdia":   self.ui.tools_defaults_form.tools_iso_group.tipdia_spinner,
-            "tools_iso_tool_vtipangle": self.ui.tools_defaults_form.tools_iso_group.tipangle_spinner,
-            "tools_iso_tool_cutz":      self.ui.tools_defaults_form.tools_iso_group.cutz_spinner,
+            "tools_iso_tool_vtipdia":   self.ui.tools_defaults_form.tools_iso_group.tipdia_entry,
+            "tools_iso_tool_vtipangle": self.ui.tools_defaults_form.tools_iso_group.tipangle_entry,
+            "tools_iso_tool_cutz":      self.ui.tools_defaults_form.tools_iso_group.cutz_entry,
             "tools_iso_newdia":         self.ui.tools_defaults_form.tools_iso_group.newdia_entry,
 
             "tools_iso_passes":         self.ui.tools_defaults_form.tools_iso_group.passes_entry,
@@ -346,6 +346,7 @@ class PreferencesUIManager:
             "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
             "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
             "tools_iso_area_shape":     self.ui.tools_defaults_form.tools_iso_group.area_shape_radio,
+            "tools_iso_plotting":       self.ui.tools_defaults_form.tools_iso_group.plotting_radio,
 
             # NCC Tool
             "tools_ncctools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
@@ -360,13 +361,13 @@ class PreferencesUIManager:
             "tools_ncc_offset_value":   self.ui.tools_defaults_form.tools_ncc_group.ncc_offset_spinner,
             "tools_nccref":             self.ui.tools_defaults_form.tools_ncc_group.select_combo,
             "tools_ncc_area_shape":     self.ui.tools_defaults_form.tools_ncc_group.area_shape_radio,
-            "tools_ncc_plotting":       self.ui.tools_defaults_form.tools_ncc_group.ncc_plotting_radio,
             "tools_nccmilling_type":    self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
             "tools_ncctool_type":       self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
             "tools_ncccutz":            self.ui.tools_defaults_form.tools_ncc_group.cutz_entry,
             "tools_ncctipdia":          self.ui.tools_defaults_form.tools_ncc_group.tipdia_entry,
             "tools_ncctipangle":        self.ui.tools_defaults_form.tools_ncc_group.tipangle_entry,
             "tools_nccnewdia":          self.ui.tools_defaults_form.tools_ncc_group.newdia_entry,
+            "tools_ncc_plotting":       self.ui.tools_defaults_form.tools_ncc_group.plotting_radio,
 
             # CutOut Tool
             "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,

+ 3 - 3
AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCDoubleSpinner, FCEntry, FloatEntry, RadioSet, FCCheckBox
+from AppGUI.GUIElements import FCDoubleSpinner, RadioSet, FCCheckBox, NumericalEvalTupleEntry, NumericalEvalEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
 import AppTranslation as fcTranslate
@@ -60,7 +60,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
         toolchange_xy_label.setToolTip(
             _("Toolchange X,Y position.")
         )
-        self.toolchangexy_entry = FCEntry()
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid1.addWidget(toolchange_xy_label, 1, 0)
         grid1.addWidget(self.toolchangexy_entry, 1, 1)
@@ -71,7 +71,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
             _("Height of the tool just after start.\n"
               "Delete the value if you don't need this feature.")
         )
-        self.estartz_entry = FloatEntry()
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
 
         grid1.addWidget(startzlabel, 2, 0)
         grid1.addWidget(self.estartz_entry, 2, 1)

+ 2 - 2
AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py

@@ -2,7 +2,7 @@ from PyQt5 import QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
 from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry, FCSpinner, OptionalInputSection, \
-    FCComboBox
+    FCComboBox, NumericalEvalTupleEntry
 from AppGUI.preferences import machinist_setting
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
@@ -198,7 +198,7 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
               "If no value is entered then there is no move\n"
               "on X,Y plane at the end of the job.")
         )
-        self.endxy_entry = FCEntry()
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid2.addWidget(endmove_xy_label, 9, 0)
         grid2.addWidget(self.endxy_entry, 9, 1)

+ 18 - 10
AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py

@@ -1,7 +1,8 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCEntry, FloatEntry, FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel, NumericalEvalTupleEntry, \
+    NumericalEvalEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -46,8 +47,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         toolchange_xy_label.setToolTip(
             _("Toolchange X,Y position.")
         )
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
         grid1.addWidget(toolchange_xy_label, 1, 0)
-        self.toolchangexy_entry = FCEntry()
         grid1.addWidget(self.toolchangexy_entry, 1, 1)
 
         # Start move Z
@@ -56,8 +58,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
             _("Height of the tool just after starting the work.\n"
               "Delete the value if you don't need this feature.")
         )
+        self.gstartz_entry = NumericalEvalEntry(border_color='#0069A9')
+
         grid1.addWidget(startzlabel, 2, 0)
-        self.gstartz_entry = FloatEntry()
         grid1.addWidget(self.gstartz_entry, 2, 1)
 
         # Feedrate rapids
@@ -186,6 +189,11 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         grid1.addWidget(segy_label, 11, 0)
         grid1.addWidget(self.segy_entry, 11, 1)
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 12, 0, 1, 2)
+
         # -----------------------------
         # --- Area Exclusion ----------
         # -----------------------------
@@ -195,10 +203,10 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
               "Those parameters are available only for\n"
               "Advanced App. Level.")
         )
-        grid1.addWidget(self.adv_label, 12, 0, 1, 2)
+        grid1.addWidget(self.adv_label, 13, 0, 1, 2)
 
         # Exclusion Area CB
-        self.exclusion_cb = FCCheckBox('%s:' % _("Exclusion areas"))
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
         self.exclusion_cb.setToolTip(
             _(
                 "Include exclusion areas.\n"
@@ -206,7 +214,7 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
                 "is forbidden."
             )
         )
-        grid1.addWidget(self.exclusion_cb, 13, 0, 1, 2)
+        grid1.addWidget(self.exclusion_cb, 14, 0, 1, 2)
 
         # Area Selection shape
         self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
@@ -217,8 +225,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
                                           {'label': _("Polygon"), 'value': 'polygon'}])
 
-        grid1.addWidget(self.area_shape_label, 14, 0)
-        grid1.addWidget(self.area_shape_radio, 14, 1)
+        grid1.addWidget(self.area_shape_label, 15, 0)
+        grid1.addWidget(self.area_shape_radio, 15, 1)
 
         # Chose Strategy
         self.strategy_label = FCLabel('%s:' % _("Strategy"))
@@ -229,8 +237,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
                                         {'label': _('Around'), 'value': 'around'}])
 
-        grid1.addWidget(self.strategy_label, 15, 0)
-        grid1.addWidget(self.strategy_radio, 15, 1)
+        grid1.addWidget(self.strategy_label, 16, 0)
+        grid1.addWidget(self.strategy_radio, 16, 1)
 
         # Over Z
         self.over_z_label = FCLabel('%s:' % _("Over Z"))

+ 3 - 2
AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py

@@ -1,7 +1,8 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
-from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCEntry, FCSpinner, FCComboBox
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCSpinner, FCComboBox, \
+    NumericalEvalTupleEntry
 from AppGUI.preferences import machinist_setting
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
@@ -176,7 +177,7 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
               "If no value is entered then there is no move\n"
               "on X,Y plane at the end of the job.")
         )
-        self.endxy_entry = FCEntry()
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid1.addWidget(endmove_xy_label, 7, 0)
         grid1.addWidget(self.endxy_entry, 7, 1)

+ 0 - 79
AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py

@@ -63,85 +63,6 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 2, 0, 1, 2)
 
-        # Tool Type
-        self.tool_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Type'))
-        self.tool_type_label.setToolTip(
-            _("Choose which tool to use for Gerber isolation:\n"
-              "'Circular' or 'V-shape'.\n"
-              "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'}])
-
-        grid0.addWidget(self.tool_type_label, 3, 0)
-        grid0.addWidget(self.tool_type_radio, 3, 1, 1, 2)
-
-        # Tip Dia
-        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
-        self.tipdialabel.setToolTip(
-            _("The tip diameter for V-Shape Tool")
-        )
-        self.tipdia_spinner = FCDoubleSpinner()
-        self.tipdia_spinner.set_precision(self.decimals)
-        self.tipdia_spinner.set_range(-99.9999, 99.9999)
-        self.tipdia_spinner.setSingleStep(0.1)
-        self.tipdia_spinner.setWrapping(True)
-        grid0.addWidget(self.tipdialabel, 4, 0)
-        grid0.addWidget(self.tipdia_spinner, 4, 1, 1, 2)
-
-        # Tip Angle
-        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
-        self.tipanglelabel.setToolTip(
-            _("The tip angle for V-Shape Tool.\n"
-              "In degree.")
-        )
-        self.tipangle_spinner = FCSpinner()
-        self.tipangle_spinner.set_range(1, 180)
-        self.tipangle_spinner.set_step(5)
-        self.tipangle_spinner.setWrapping(True)
-        grid0.addWidget(self.tipanglelabel, 5, 0)
-        grid0.addWidget(self.tipangle_spinner, 5, 1, 1, 2)
-
-        # Cut Z
-        self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
-        self.cutzlabel.setToolTip(
-            _("Cutting depth (negative)\n"
-              "below the copper surface.")
-        )
-        self.cutz_spinner = FCDoubleSpinner()
-        self.cutz_spinner.set_precision(self.decimals)
-        self.cutz_spinner.set_range(-99.9999, 0.0000)
-        self.cutz_spinner.setSingleStep(0.1)
-        self.cutz_spinner.setWrapping(True)
-
-        grid0.addWidget(self.cutzlabel, 6, 0)
-        grid0.addWidget(self.cutz_spinner, 6, 1, 1, 2)
-
-        # Isolation Type
-        self.iso_type_label = QtWidgets.QLabel('%s:' % _('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': _('Exterior'), 'value': 'ext'},
-                                        {'label': _('Interior'), 'value': 'int'}])
-
-        grid0.addWidget(self.iso_type_label, 7, 0,)
-        grid0.addWidget(self.iso_type_radio, 7, 1, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 8, 0, 1, 2)
-
         # Buffering Type
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label.setToolTip(

+ 3 - 2
AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet
+from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -109,8 +109,9 @@ class GerberEditorPrefGroupUI(OptionsGroupUI):
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
         )
+        self.adddim_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
         grid0.addWidget(self.adddim_label, 5, 0)
-        self.adddim_entry = FCEntry()
         grid0.addWidget(self.adddim_entry, 5, 1)
 
         self.grb_array_linear_label = QtWidgets.QLabel('<b>%s:</b>' % _('Linear Pad Array'))

+ 0 - 90
AppGUI/preferences/gerber/GerberOptPrefGroupUI.py

@@ -28,96 +28,6 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
 
         self.setTitle(str(_("Gerber Options")))
 
-        # ## Isolation Routing
-        self.isolation_routing_label = QtWidgets.QLabel("<b>%s:</b>" % _("Isolation Routing"))
-        self.isolation_routing_label.setToolTip(
-            _("Create a Geometry object with\n"
-              "toolpaths to cut outside polygons.")
-        )
-        self.layout.addWidget(self.isolation_routing_label)
-
-        # Cutting Tool Diameter
-        grid0 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid0)
-
-        tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
-        tdlabel.setToolTip(
-            _("Diameter of the cutting tool.")
-        )
-        grid0.addWidget(tdlabel, 0, 0)
-        self.iso_tool_dia_entry = FCDoubleSpinner()
-        self.iso_tool_dia_entry.set_precision(self.decimals)
-        self.iso_tool_dia_entry.setSingleStep(0.1)
-        self.iso_tool_dia_entry.set_range(-9999, 9999)
-
-        grid0.addWidget(self.iso_tool_dia_entry, 0, 1)
-
-        # Nr of passes
-        passlabel = QtWidgets.QLabel('%s:' % _('# Passes'))
-        passlabel.setToolTip(
-            _("Width of the isolation gap in\n"
-              "number (integer) of tool widths.")
-        )
-        self.iso_width_entry = FCSpinner()
-        self.iso_width_entry.set_range(1, 999)
-
-        grid0.addWidget(passlabel, 1, 0)
-        grid0.addWidget(self.iso_width_entry, 1, 1)
-
-        # Pass overlap
-        overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
-        overlabel.setToolTip(
-            _("How much (percentage) of the tool width to overlap each tool pass.")
-        )
-        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.0000, 99.9999)
-        self.iso_overlap_entry.setSingleStep(0.1)
-
-        grid0.addWidget(overlabel, 2, 0)
-        grid0.addWidget(self.iso_overlap_entry, 2, 1)
-
-        # Isolation Scope
-        self.select_label = QtWidgets.QLabel('%s:' % _('Selection'))
-        self.select_label.setToolTip(
-            _("Isolation scope. Choose what to isolate:\n"
-              "- 'All' -> Isolate all the polygons in the object\n"
-              "- 'Selection' -> Isolate a selection of polygons.\n"
-              "- 'Reference Object' - will process the area specified by another object.")
-        )
-        self.select_combo = FCComboBox()
-        self.select_combo.addItems(
-            [_("All"), _("Area Selection"), _("Reference Object")]
-        )
-
-        grid0.addWidget(self.select_label, 3, 0)
-        grid0.addWidget(self.select_combo, 3, 1, 1, 2)
-
-        # Milling Type
-        milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
-        milling_type_label.setToolTip(
-            _("Milling type:\n"
-              "- climb / best for precision milling and to reduce tool usage\n"
-              "- conventional / useful when there is no backlash compensation")
-        )
-        grid0.addWidget(milling_type_label, 4, 0)
-        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'label': _('Conventional'), 'value': 'cv'}])
-        grid0.addWidget(self.milling_type_radio, 4, 1)
-
-        # Combine passes
-        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
-        self.combine_passes_cb.setToolTip(
-            _("Combine all passes into one object")
-        )
-        grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 6, 0, 1, 2)
-
         # ## Clear non-copper regions
         self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
         self.clearcopper_label.setToolTip(

+ 1 - 0
AppGUI/preferences/gerber/GerberPreferencesUI.py

@@ -44,6 +44,7 @@ class GerberPreferencesUI(QtWidgets.QWidget):
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.gerber_opt_group)
         self.vlay.addWidget(self.gerber_exp_group)
+        self.vlay.addStretch()
 
         self.layout.addWidget(self.gerber_gen_group)
         self.layout.addLayout(self.vlay)

+ 2 - 2
AppGUI/preferences/tools/Tools2CalPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -116,7 +116,7 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
               "(x, y) point will be used,")
         )
 
-        self.toolchange_xy_entry = FCEntry()
+        self.toolchange_xy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid_lay.addWidget(toolchangexy_lbl, 7, 0)
         grid_lay.addWidget(self.toolchange_xy_entry, 7, 1, 1, 2)

+ 29 - 29
AppGUI/preferences/tools/ToolsISOPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -32,7 +32,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
             _("Create a Geometry object with\n"
               "toolpaths to cut around polygons.")
         )
-        self.layout.addWidget(self.clearcopper_label)
+        self.layout.addWidget(self.iso_label)
 
         grid0 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0)
@@ -44,11 +44,11 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
         )
-        self.tool_dia_entry = FCEntry(border_color='#0069A9')
+        self.tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
         self.tool_dia_entry.setPlaceholderText(_("Comma separated values"))
 
         grid0.addWidget(isotdlabel, 0, 0)
-        grid0.addWidget(self.tool_dia_entry, 0, 1)
+        grid0.addWidget(self.tool_dia_entry, 0, 1, 1, 2)
 
         # Tool order Radio Button
         self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
@@ -64,7 +64,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
                                      {'label': _('Reverse'), 'value': 'rev'}])
 
         grid0.addWidget(self.order_label, 1, 0)
-        grid0.addWidget(self.order_radio, 1, 1)
+        grid0.addWidget(self.order_radio, 1, 1, 1, 2)
 
         # Tool Type Radio Button
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
@@ -83,7 +83,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         )
 
         grid0.addWidget(self.tool_type_label, 2, 0)
-        grid0.addWidget(self.tool_type_radio, 2, 1)
+        grid0.addWidget(self.tool_type_radio, 2, 1, 1, 2)
 
         # Tip Dia
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
@@ -95,7 +95,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.tipdia_entry.setSingleStep(0.1)
 
         grid0.addWidget(self.tipdialabel, 3, 0)
-        grid0.addWidget(self.tipdia_entry, 3, 1)
+        grid0.addWidget(self.tipdia_entry, 3, 1, 1, 2)
 
         # Tip Angle
         self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
@@ -109,7 +109,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.tipangle_entry.setWrapping(True)
 
         grid0.addWidget(self.tipanglelabel, 4, 0)
-        grid0.addWidget(self.tipangle_entry, 4, 1)
+        grid0.addWidget(self.tipangle_entry, 4, 1, 1, 2)
 
         # Cut Z entry
         cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
@@ -128,7 +128,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         )
 
         grid0.addWidget(cutzlabel, 5, 0)
-        grid0.addWidget(self.cutz_entry, 5, 1)
+        grid0.addWidget(self.cutz_entry, 5, 1, 1, 2)
 
         # New Diameter
         self.newdialabel = QtWidgets.QLabel('%s:' % _('New Dia'))
@@ -143,12 +143,12 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.newdia_entry.setSingleStep(0.1)
 
         grid0.addWidget(self.newdialabel, 6, 0)
-        grid0.addWidget(self.newdia_entry, 6, 1)
+        grid0.addWidget(self.newdia_entry, 6, 1, 1, 2)
 
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 7, 0, 1, 2)
+        grid0.addWidget(separator_line, 7, 0, 1, 3)
 
         # Passes
         passlabel = QtWidgets.QLabel('%s:' % _('Passes'))
@@ -161,7 +161,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.passes_entry.setObjectName("i_passes")
 
         grid0.addWidget(passlabel, 8, 0)
-        grid0.addWidget(self.passes_entry, 8, 1)
+        grid0.addWidget(self.passes_entry, 8, 1, 1, 2)
 
         # Overlap Entry
         overlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
@@ -176,7 +176,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.overlap_entry.setObjectName("i_overlap")
 
         grid0.addWidget(overlabel, 9, 0)
-        grid0.addWidget(self.overlap_entry, 9, 1)
+        grid0.addWidget(self.overlap_entry, 9, 1, 1, 2)
 
         # Milling Type Radio Button
         self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
@@ -195,7 +195,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         )
 
         grid0.addWidget(self.milling_type_label, 10, 0)
-        grid0.addWidget(self.milling_type_radio, 10, 1)
+        grid0.addWidget(self.milling_type_radio, 10, 1, 1, 2)
 
         # Follow
         self.follow_label = QtWidgets.QLabel('%s:' % _('Follow'))
@@ -212,7 +212,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.follow_cb.setObjectName("i_follow")
 
         grid0.addWidget(self.follow_label, 11, 0)
-        grid0.addWidget(self.follow_cb, 11, 1)
+        grid0.addWidget(self.follow_cb, 11, 1, 1, 2)
 
         # Isolation Type
         self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
@@ -232,15 +232,15 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.iso_type_radio.setObjectName("i_type")
 
         grid0.addWidget(self.iso_type_label, 12, 0)
-        grid0.addWidget(self.iso_type_radio, 12, 1)
+        grid0.addWidget(self.iso_type_radio, 12, 1, 1, 2)
 
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 13, 0, 1, 2)
+        grid0.addWidget(separator_line, 13, 0, 1, 3)
 
         # Rest machining CheckBox
-        self.rest_cb = FCCheckBox('%s' % _("Rest Machining"))
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
         self.rest_cb.setObjectName("i_rest_machining")
         self.rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
@@ -252,7 +252,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
               "If not checked, use the standard algorithm.")
         )
 
-        grid0.addWidget(self.ncc_rest_cb, 17, 0, 1, 2)
+        grid0.addWidget(self.rest_cb, 17, 0)
 
         # Combine All Passes
         self.combine_passes_cb = FCCheckBox(label=_('Combine'))
@@ -261,7 +261,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         )
         self.combine_passes_cb.setObjectName("i_combine")
 
-        grid0.addWidget(self.combine_passes_cb, 18, 0, 1, 2)
+        grid0.addWidget(self.combine_passes_cb, 17, 1)
 
         # Exception Areas
         self.except_cb = FCCheckBox(label=_('Except'))
@@ -269,7 +269,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
                                     "by checking this, the area of the object below\n"
                                     "will be subtracted from the isolation geometry."))
         self.except_cb.setObjectName("i_except")
-        grid0.addWidget(self.except_cb, 19, 0, 1, 2)
+        grid0.addWidget(self.except_cb, 17, 2)
 
         # Isolation Scope
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
@@ -286,7 +286,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.select_combo.setObjectName("i_selection")
 
         grid0.addWidget(self.select_label, 20, 0)
-        grid0.addWidget(self.select_combo, 20, 1)
+        grid0.addWidget(self.select_combo, 20, 1, 1, 2)
 
         # Area Shape
         self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
@@ -298,22 +298,22 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
                                           {'label': _("Polygon"), 'value': 'polygon'}])
 
         grid0.addWidget(self.area_shape_label, 21, 0)
-        grid0.addWidget(self.area_shape_radio, 21, 1)
+        grid0.addWidget(self.area_shape_radio, 21, 1, 1, 2)
 
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 22, 0, 1, 2)
+        grid0.addWidget(separator_line, 22, 0, 1, 3)
 
         # ## Plotting type
-        self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
-                                            {"label": _("Progressive"), "value": "progressive"}])
-        plotting_label = QtWidgets.QLabel('%s:' % _("ISO Plotting"))
+        self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
+                                        {"label": _("Progressive"), "value": "progressive"}])
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
         plotting_label.setToolTip(
             _("- 'Normal' -  normal plotting, done at the end of the job\n"
               "- 'Progressive' - each shape is plotted after it is generated")
         )
-        grid0.addWidget(plotting_label, 21, 0)
-        grid0.addWidget(self.ncc_plotting_radio, 21, 1)
+        grid0.addWidget(plotting_label, 23, 0)
+        grid0.addWidget(self.plotting_radio, 23, 1, 1, 2)
 
         self.layout.addStretch()

+ 7 - 7
AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -45,7 +45,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
               "Valid values: 0.3, 1.0")
         )
         grid0.addWidget(ncctdlabel, 0, 0)
-        self.ncc_tool_dia_entry = FCEntry(border_color='#0069A9')
+        self.ncc_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
         self.ncc_tool_dia_entry.setPlaceholderText(_("Comma separated values"))
         grid0.addWidget(self.ncc_tool_dia_entry, 0, 1)
 
@@ -285,7 +285,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 16, 0, 1, 2)
 
         # Rest machining CheckBox
-        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest Machining"))
+        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest"))
         self.ncc_rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
               "Basically it will clear copper outside PCB features,\n"
@@ -336,14 +336,14 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 20, 0, 1, 2)
 
         # ## Plotting type
-        self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
-                                            {"label": _("Progressive"), "value": "progressive"}])
-        plotting_label = QtWidgets.QLabel('%s:' % _("NCC Plotting"))
+        self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
+                                        {"label": _("Progressive"), "value": "progressive"}])
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
         plotting_label.setToolTip(
             _("- 'Normal' -  normal plotting, done at the end of the job\n"
               "- 'Progressive' - each shape is plotted after it is generated")
         )
         grid0.addWidget(plotting_label, 21, 0)
-        grid0.addWidget(self.ncc_plotting_radio, 21, 1)
+        grid0.addWidget(self.plotting_radio, 21, 1)
 
         self.layout.addStretch()

+ 5 - 5
AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -53,7 +53,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(ptdlabel, 0, 0)
 
-        self.painttooldia_entry = FCEntry(border_color='#0069A9')
+        self.painttooldia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
         self.painttooldia_entry.setPlaceholderText(_("Comma separated values"))
 
         grid0.addWidget(self.painttooldia_entry, 0, 1)
@@ -241,8 +241,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 13, 0, 1, 2)
 
-        self.rest_cb = FCCheckBox('%s' % _("Rest Machining"))
-        self.rest_cb.setObjectName(_("Rest Machining"))
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.rest_cb.setObjectName(_("Rest"))
         self.rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
               "Basically it will clear copper outside PCB features,\n"
@@ -302,7 +302,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         # ## Plotting type
         self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
                                               {"label": _("Progressive"), "value": "progressive"}])
-        plotting_label = QtWidgets.QLabel('%s:' % _("Paint Plotting"))
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
         plotting_label.setToolTip(
             _("- 'Normal' -  normal plotting, done at the end of the job\n"
               "- 'Progressive' - each shape is plotted after it is generated")

+ 3 - 3
AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCEntry, FCDoubleSpinner, FCSpinner, FCComboBox
+from AppGUI.GUIElements import FCDoubleSpinner, FCSpinner, FCComboBox, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -45,7 +45,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
         )
-        self.nozzle_tool_dia_entry = FCEntry()
+        self.nozzle_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid0.addWidget(nozzletdlabel, 0, 0)
         grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1)
@@ -130,7 +130,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.z_toolchange_entry, 6, 1)
 
         # X,Y Toolchange location
-        self.xy_toolchange_entry = FCEntry()
+        self.xy_toolchange_entry = NumericalEvalTupleEntry(border_color='#0069A9')
         self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y"))
         self.xy_toolchange_label.setToolTip(
             _("The X,Y location for tool (nozzle) change.\n"

+ 2 - 2
AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCEntry
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -191,7 +191,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
               "The 'x' in (x, y) will be used when using Flip on X and\n"
               "the 'y' in (x, y) will be used when using Flip on Y and")
         )
-        self.flip_ref_entry = FCEntry()
+        self.flip_ref_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid0.addWidget(self.flip_ref_label, 14, 0, 1, 2)
         grid0.addWidget(self.flip_ref_entry, 15, 0, 1, 2)

+ 33 - 9
AppObjects/FlatCAMExcellon.py

@@ -1180,13 +1180,25 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
     def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
         """
+        Will generate an Geometry Object allowing to cut a drill hole instead of drilling it.
+
         Note: This method is a good template for generic operations as
         it takes it's options from parameters or otherwise from the
         object's options and returns a (success, msg) tuple as feedback
         for shell operations.
 
-        :return:    Success/failure condition tuple (bool, str).
-        :rtype:     tuple
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
         """
 
         # Get the tools from the list. These are keys
@@ -1250,7 +1262,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
-
+            geo_obj.options["multidepth"] = self.options["multidepth"]
             geo_obj.solid_geometry = []
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
@@ -1280,15 +1292,27 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
         return True, ""
 
-    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=True, use_thread=False):
+    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
         """
+        Will generate an Geometry Object allowing to cut/mill a slot hole.
+
         Note: This method is a good template for generic operations as
         it takes it's options from parameters or otherwise from the
         object's options and returns a (success, msg) tuple as feedback
         for shell operations.
 
-        :return: Success/failure condition tuple (bool, str).
-        :rtype: tuple
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
         """
 
         # Get the tools from the list. These are keys
@@ -1341,7 +1365,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
-
+            geo_obj.options["multidepth"] = self.options["multidepth"]
             geo_obj.solid_geometry = []
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
@@ -1388,13 +1412,13 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.app.defaults.report_usage("excellon_on_create_milling_drills button")
         self.read_form()
 
-        self.generate_milling_drills(use_thread=False)
+        self.generate_milling_drills(use_thread=False, plot=True)
 
     def on_generate_milling_slots_button_click(self, *args):
         self.app.defaults.report_usage("excellon_on_create_milling_slots_button")
         self.read_form()
 
-        self.generate_milling_slots(use_thread=False)
+        self.generate_milling_slots(use_thread=False, plot=True)
 
     def on_pp_changed(self):
         current_pp = self.ui.pp_excellon_name_cb.get_value()

+ 3 - 688
AppObjects/FlatCAMGerber.py

@@ -115,23 +115,12 @@ class GerberObject(FlatCAMObj, Gerber):
             "plot": True,
             "multicolored": False,
             "solid": False,
-            "tool_type": 'circular',
-            "vtipdia": 0.1,
-            "vtipangle": 30,
-            "vcutz": -0.05,
-            "isotooldia": 0.016,
-            "isopasses": 1,
-            "isooverlap": 15,
-            "milling_type": "cl",
-            "combine_passes": True,
             "noncoppermargin": 0.0,
             "noncopperrounded": False,
             "bboxmargin": 0.0,
             "bboxrounded": False,
             "aperture_display": False,
             "follow": False,
-            "iso_scope": 'all',
-            "iso_type": 'full'
         })
 
         # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
@@ -197,33 +186,22 @@ class GerberObject(FlatCAMObj, Gerber):
             "plot": self.ui.plot_cb,
             "multicolored": self.ui.multicolored_cb,
             "solid": self.ui.solid_cb,
-            "tool_type": self.ui.tool_type_radio,
-            "vtipdia": self.ui.tipdia_spinner,
-            "vtipangle": self.ui.tipangle_spinner,
-            "vcutz": self.ui.cutz_spinner,
-            "isotooldia": self.ui.iso_tool_dia_entry,
-            "isopasses": self.ui.iso_width_entry,
-            "isooverlap": self.ui.iso_overlap_entry,
-            "milling_type": self.ui.milling_type_radio,
-            "combine_passes": self.ui.combine_passes_cb,
             "noncoppermargin": self.ui.noncopper_margin_entry,
             "noncopperrounded": self.ui.noncopper_rounded_cb,
             "bboxmargin": self.ui.bbmargin_entry,
             "bboxrounded": self.ui.bbrounded_cb,
             "aperture_display": self.ui.aperture_table_visibility_cb,
-            "follow": self.ui.follow_cb,
-            "iso_scope": self.ui.iso_scope_radio,
-            "iso_type": self.ui.iso_type_radio
+            "follow": self.ui.follow_cb
         })
 
         # Fill form fields only on object create
         self.to_form()
 
         assert isinstance(self.ui, GerberObjectUI)
+
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
-        self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
 
         # Tools
         self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
@@ -235,54 +213,17 @@ class GerberObject(FlatCAMObj, Gerber):
         self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change)
         self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click)
 
-        # set the model for the Area Exception comboboxes
-        self.ui.obj_combo.setModel(self.app.collection)
-        self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.ui.obj_combo.is_last = True
-        self.ui.obj_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Geometry"): "Geometry"
-        }[self.ui.type_obj_combo.get_value()]
-        self.on_type_obj_index_changed()
-
-        self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
-
-        self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type_change)
-        # establish visibility for the GUI elements found in the slot function
-        self.ui.tool_type_radio.activated_custom.emit(self.options['tool_type'])
-
         # Show/Hide Advanced Options
         if self.app.defaults["global_app_level"] == 'b':
             self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
-            self.options['tool_type'] = 'circular'
-
-            self.ui.tool_type_label.hide()
-            self.ui.tool_type_radio.hide()
-
-            # override the Preferences Value; in Basic mode the Tool Type is always Circular ('C1')
-            self.ui.tool_type_radio.set_value('circular')
-
-            self.ui.tipdialabel.hide()
-            self.ui.tipdia_spinner.hide()
-            self.ui.tipanglelabel.hide()
-            self.ui.tipangle_spinner.hide()
-            self.ui.cutzlabel.hide()
-            self.ui.cutz_spinner.hide()
 
             self.ui.apertures_table_label.hide()
             self.ui.aperture_table_visibility_cb.hide()
-            self.ui.milling_type_label.hide()
-            self.ui.milling_type_radio.hide()
-            self.ui.iso_type_label.hide()
-            self.ui.iso_type_radio.hide()
 
             self.ui.follow_cb.hide()
-            self.ui.except_cb.setChecked(False)
-            self.ui.except_cb.hide()
+
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
-            self.ui.tipdia_spinner.valueChanged.connect(self.on_calculate_tooldia)
-            self.ui.tipangle_spinner.valueChanged.connect(self.on_calculate_tooldia)
-            self.ui.cutz_spinner.valueChanged.connect(self.on_calculate_tooldia)
 
         if self.app.defaults["gerber_buffering"] == 'no':
             self.ui.create_buffer_button.show()
@@ -300,58 +241,6 @@ class GerberObject(FlatCAMObj, Gerber):
         self.build_ui()
         self.units_found = self.app.defaults['units']
 
-    def on_calculate_tooldia(self):
-        try:
-            tdia = float(self.ui.tipdia_spinner.get_value())
-        except Exception:
-            return
-        try:
-            dang = float(self.ui.tipangle_spinner.get_value())
-        except Exception:
-            return
-        try:
-            cutz = float(self.ui.cutz_spinner.get_value())
-        except Exception:
-            return
-
-        cutz *= -1
-        if cutz < 0:
-            cutz *= -1
-
-        half_tip_angle = dang / 2
-
-        tool_diameter = tdia + (2 * cutz * math.tan(math.radians(half_tip_angle)))
-        self.ui.iso_tool_dia_entry.set_value(tool_diameter)
-
-    def on_type_obj_index_changed(self):
-        val = self.ui.type_obj_combo.get_value()
-        obj_type = {"Gerber": 0, "Geometry": 2}[val]
-        self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.ui.obj_combo.setCurrentIndex(0)
-        self.ui.obj_combo.obj_type = {_("Gerber"): "Gerber", _("Geometry"): "Geometry"}[val]
-
-    def on_tool_type_change(self, state):
-        if state == 'circular':
-            self.ui.tipdialabel.hide()
-            self.ui.tipdia_spinner.hide()
-            self.ui.tipanglelabel.hide()
-            self.ui.tipangle_spinner.hide()
-            self.ui.cutzlabel.hide()
-            self.ui.cutz_spinner.hide()
-            self.ui.iso_tool_dia_entry.setDisabled(False)
-            # update the value in the self.iso_tool_dia_entry once this is selected
-            self.ui.iso_tool_dia_entry.set_value(self.options['isotooldia'])
-        else:
-            self.ui.tipdialabel.show()
-            self.ui.tipdia_spinner.show()
-            self.ui.tipanglelabel.show()
-            self.ui.tipangle_spinner.show()
-            self.ui.cutzlabel.show()
-            self.ui.cutz_spinner.show()
-            self.ui.iso_tool_dia_entry.setDisabled(True)
-            # update the value in the self.iso_tool_dia_entry once this is selected
-            self.on_calculate_tooldia()
-
     def build_ui(self):
         FlatCAMObj.build_ui(self)
 
@@ -562,521 +451,6 @@ class GerberObject(FlatCAMObj, Gerber):
 
         self.app.app_obj.new_object("geometry", name, geo_init)
 
-    def on_iso_button_click(self, *args):
-
-        obj = self.app.collection.get_active()
-
-        self.iso_type = 2
-        if self.ui.iso_type_radio.get_value() == 'ext':
-            self.iso_type = 0
-        if self.ui.iso_type_radio.get_value() == 'int':
-            self.iso_type = 1
-
-        def worker_task(iso_obj, app_obj):
-            with self.app.proc_container.new(_("Isolating...")):
-                if self.ui.follow_cb.get_value() is True:
-                    iso_obj.follow_geo()
-                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
-                    iso_obj.ui.plot_cb.toggle()
-                else:
-                    app_obj.defaults.report_usage("gerber_on_iso_button")
-                    self.read_form()
-
-                    iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single'
-                    self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
-
-        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
-
-    def follow_geo(self, outname=None):
-        """
-        Creates a geometry object "following" the gerber paths.
-
-        :return: None
-        """
-
-        # default_name = self.options["name"] + "_follow"
-        # follow_name = outname or default_name
-
-        if outname is None:
-            follow_name = self.options["name"] + "_follow"
-        else:
-            follow_name = outname
-
-        def follow_init(follow_obj, app):
-            # Propagate options
-            follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-            follow_obj.solid_geometry = self.follow_geometry
-
-        # TODO: Do something if this is None. Offer changing name?
-        try:
-            self.app.app_obj.new_object("geometry", follow_name, follow_init)
-        except Exception as e:
-            return "Operation failed: %s" % str(e)
-
-    def isolate_handler(self, iso_type, iso_scope):
-
-        if iso_scope == 'all':
-            self.isolate(iso_type=iso_type)
-        else:
-            # 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_mouse_click_release)
-
-            if self.app.is_legacy is False:
-                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.mr)
-
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
-
-    def on_mouse_click_release(self, event):
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            right_button = 2
-            self.app.event_is_dragging = self.app.event_is_dragging
-        else:
-            event_pos = (event.xdata, event.ydata)
-            right_button = 3
-            self.app.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)
-        if self.app.grid_status():
-            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
-        else:
-            curr_pos = (curr_pos[0], curr_pos[1])
-
-        if event.button == 1:
-            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]))
-
-            if self.app.selection_type is not None:
-                self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
-                self.app.selection_type = None
-            elif clicked_poly:
-                if clicked_poly not in self.poly_dict.values():
-                    shape_id = self.app.tool_shapes.add(tolerance=self.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 isolation."))
-                    )
-                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 isolation."))
-                    )
-
-                self.app.tool_shapes.redraw()
-            else:
-                self.app.inform.emit(_("No polygon detected under click position."))
-        elif event.button == right_button and self.app.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_mouse_click_release)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.mr)
-
-            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.isolate(iso_type=self.iso_type, geometry=poly_list)
-                self.poly_dict.clear()
-            else:
-                self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
-
-    def selection_area_handler(self, start_pos, end_pos, sel_type):
-        """
-        :param start_pos: mouse position when the selection LMB click was done
-        :param end_pos: mouse position when the left mouse button is released
-        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
-        :return:
-        """
-        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
-
-        # delete previous selection shape
-        self.app.delete_selection_shape()
-
-        added_poly_count = 0
-        try:
-            for geo in self.solid_geometry:
-                if geo not in self.poly_dict.values():
-                    if sel_type is True:
-                        if geo.within(poly_selection):
-                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                                shape=geo,
-                                                                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] = geo
-                            added_poly_count += 1
-                    else:
-                        if poly_selection.intersects(geo):
-                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                                shape=geo,
-                                                                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] = geo
-                            added_poly_count += 1
-        except TypeError:
-            if self.solid_geometry not in self.poly_dict.values():
-                if sel_type is True:
-                    if self.solid_geometry.within(poly_selection):
-                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                            shape=self.solid_geometry,
-                                                            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] = self.solid_geometry
-                        added_poly_count += 1
-                else:
-                    if poly_selection.intersects(self.solid_geometry):
-                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                            shape=self.solid_geometry,
-                                                            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] = self.solid_geometry
-                        added_poly_count += 1
-
-        if added_poly_count > 0:
-            self.app.tool_shapes.redraw()
-            self.app.inform.emit(
-                '%s: %d. %s' % (_("Added polygon"),
-                                int(added_poly_count),
-                                _("Click to add next polygon or right click to start isolation."))
-            )
-        else:
-            self.app.inform.emit(_("No polygon in selection."))
-
-    def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
-                milling_type=None, follow=None, plot=True):
-        """
-        Creates an isolation routing geometry object in the project.
-
-        :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
-        :param geometry: specific geometry to isolate
-        :param dia: Tool diameter
-        :param passes: Number of tool widths to cut
-        :param overlap: Overlap between passes in fraction of tool diameter
-        :param outname: Base name of the output object
-        :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
-        :param milling_type: type of milling: conventional or climbing
-        :param follow: Boolean: if to generate a 'follow' geometry
-        :param plot: Boolean: if to plot the resulting geometry object
-        :return: None
-        """
-
-        if geometry is None:
-            work_geo = self.follow_geometry if follow is True else self.solid_geometry
-        else:
-            work_geo = geometry
-
-        if dia is None:
-            dia = float(self.options["isotooldia"])
-
-        if passes is None:
-            passes = int(self.options["isopasses"])
-
-        if overlap is None:
-            overlap = float(self.options["isooverlap"])
-
-        overlap /= 100.0
-
-        combine = self.options["combine_passes"] if combine is None else bool(combine)
-
-        if milling_type is None:
-            milling_type = self.options["milling_type"]
-
-        if iso_type is None:
-            iso_t = 2
-        else:
-            iso_t = iso_type
-
-        base_name = self.options["name"]
-
-        if combine:
-            if outname is None:
-                if self.iso_type == 0:
-                    iso_name = base_name + "_ext_iso"
-                elif self.iso_type == 1:
-                    iso_name = base_name + "_int_iso"
-                else:
-                    iso_name = base_name + "_iso"
-            else:
-                iso_name = outname
-
-            def iso_init(geo_obj, app_obj):
-                # Propagate options
-                geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper()
-
-                geo_obj.solid_geometry = []
-
-                # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                if self.ui.tool_type_radio.get_value() == 'v':
-                    new_cutz = self.ui.cutz_spinner.get_value()
-                    new_vtipdia = self.ui.tipdia_spinner.get_value()
-                    new_vtipangle = self.ui.tipangle_spinner.get_value()
-                    tool_type = 'V'
-                else:
-                    new_cutz = self.app.defaults['geometry_cutz']
-                    new_vtipdia = self.app.defaults['geometry_vtipdia']
-                    new_vtipangle = self.app.defaults['geometry_vtipangle']
-                    tool_type = 'C1'
-
-                # store here the default data for Geometry Data
-                default_data = {}
-                default_data.update({
-                    "name": iso_name,
-                    "plot": self.app.defaults['geometry_plot'],
-                    "cutz": new_cutz,
-                    "vtipdia": new_vtipdia,
-                    "vtipangle": new_vtipangle,
-                    "travelz": self.app.defaults['geometry_travelz'],
-                    "feedrate": self.app.defaults['geometry_feedrate'],
-                    "feedrate_z": self.app.defaults['geometry_feedrate_z'],
-                    "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
-                    "dwell": self.app.defaults['geometry_dwell'],
-                    "dwelltime": self.app.defaults['geometry_dwelltime'],
-                    "multidepth": self.app.defaults['geometry_multidepth'],
-                    "ppname_g": self.app.defaults['geometry_ppname_g'],
-                    "depthperpass": self.app.defaults['geometry_depthperpass'],
-                    "extracut": self.app.defaults['geometry_extracut'],
-                    "extracut_length": self.app.defaults['geometry_extracut_length'],
-                    "toolchange": self.app.defaults['geometry_toolchange'],
-                    "toolchangez": self.app.defaults['geometry_toolchangez'],
-                    "endz": self.app.defaults['geometry_endz'],
-                    "spindlespeed": self.app.defaults['geometry_spindlespeed'],
-                    "toolchangexy": self.app.defaults['geometry_toolchangexy'],
-                    "startz": self.app.defaults['geometry_startz']
-                })
-
-                geo_obj.tools = {}
-                geo_obj.tools['1'] = {}
-                geo_obj.tools.update({
-                    '1': {
-                        'tooldia': float(self.options["isotooldia"]),
-                        'offset': 'Path',
-                        'offset_value': 0.0,
-                        'type': _('Rough'),
-                        'tool_type': tool_type,
-                        'data': default_data,
-                        'solid_geometry': geo_obj.solid_geometry
-                    }
-                })
-
-                for nr_pass in range(passes):
-                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow, nr_passes=nr_pass)
-
-                    if geom == 'fail':
-                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
-                    geo_obj.solid_geometry.append(geom)
-
-                    # update the geometry in the tools
-                    geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry
-
-                # detect if solid_geometry is empty and this require list flattening which is "heavy"
-                # or just looking in the lists (they are one level depth) and if any is not empty
-                # proceed with object creation, if there are empty and the number of them is the length
-                # of the list then we have an empty solid_geometry which should raise a Custom Exception
-                empty_cnt = 0
-                if not isinstance(geo_obj.solid_geometry, list) and \
-                        not isinstance(geo_obj.solid_geometry, MultiPolygon):
-                    geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                for g in geo_obj.solid_geometry:
-                    if g:
-                        break
-                    else:
-                        empty_cnt += 1
-
-                if empty_cnt == len(geo_obj.solid_geometry):
-                    raise ValidationError("Empty Geometry", None)
-                else:
-                    app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
-
-                # even if combine is checked, one pass is still single-geo
-                geo_obj.multigeo = True if passes > 1 else False
-
-                # ############################################################
-                # ########## AREA SUBTRACTION ################################
-                # ############################################################
-                if self.ui.except_cb.get_value():
-                    self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                    geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
-
-            # TODO: Do something if this is None. Offer changing name?
-            self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
-        else:
-            for i in range(passes):
-                offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia)
-                if passes > 1:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso" + str(i + 1)
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso" + str(i + 1)
-                        else:
-                            iso_name = base_name + "_iso" + str(i + 1)
-                    else:
-                        iso_name = outname
-                else:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso"
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso"
-                        else:
-                            iso_name = base_name + "_iso"
-                    else:
-                        iso_name = outname
-
-                def iso_init(geo_obj, fc_obj):
-                    # Propagate options
-                    geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                    if self.ui.tool_type_radio.get_value() == 'v':
-                        geo_obj.tool_type = 'V'
-                    else:
-                        geo_obj.tool_type = 'C1'
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow,
-                                                  nr_passes=i)
-
-                    if geom == 'fail':
-                        fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
-
-                    geo_obj.solid_geometry = geom
-
-                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                    # even if the resulting geometry is not multigeo we add the tools dict which will hold the data
-                    # required to be transfered to the Geometry object
-                    if self.ui.tool_type_radio.get_value() == 'v':
-                        new_cutz = self.ui.cutz_spinner.get_value()
-                        new_vtipdia = self.ui.tipdia_spinner.get_value()
-                        new_vtipangle = self.ui.tipangle_spinner.get_value()
-                        tool_type = 'V'
-                    else:
-                        new_cutz = self.app.defaults['geometry_cutz']
-                        new_vtipdia = self.app.defaults['geometry_vtipdia']
-                        new_vtipangle = self.app.defaults['geometry_vtipangle']
-                        tool_type = 'C1'
-
-                    # store here the default data for Geometry Data
-                    default_data = {}
-                    default_data.update({
-                        "name": iso_name,
-                        "plot": self.app.defaults['geometry_plot'],
-                        "cutz": new_cutz,
-                        "vtipdia": new_vtipdia,
-                        "vtipangle": new_vtipangle,
-                        "travelz": self.app.defaults['geometry_travelz'],
-                        "feedrate": self.app.defaults['geometry_feedrate'],
-                        "feedrate_z": self.app.defaults['geometry_feedrate_z'],
-                        "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
-                        "dwell": self.app.defaults['geometry_dwell'],
-                        "dwelltime": self.app.defaults['geometry_dwelltime'],
-                        "multidepth": self.app.defaults['geometry_multidepth'],
-                        "ppname_g": self.app.defaults['geometry_ppname_g'],
-                        "depthperpass": self.app.defaults['geometry_depthperpass'],
-                        "extracut": self.app.defaults['geometry_extracut'],
-                        "extracut_length": self.app.defaults['geometry_extracut_length'],
-                        "toolchange": self.app.defaults['geometry_toolchange'],
-                        "toolchangez": self.app.defaults['geometry_toolchangez'],
-                        "endz": self.app.defaults['geometry_endz'],
-                        "spindlespeed": self.app.defaults['geometry_spindlespeed'],
-                        "toolchangexy": self.app.defaults['geometry_toolchangexy'],
-                        "startz": self.app.defaults['geometry_startz']
-                    })
-
-                    geo_obj.tools = {}
-                    geo_obj.tools['1'] = {}
-                    geo_obj.tools.update({
-                        '1': {
-                            'tooldia': float(self.options["isotooldia"]),
-                            'offset': 'Path',
-                            'offset_value': 0.0,
-                            'type': _('Rough'),
-                            'tool_type': tool_type,
-                            'data': default_data,
-                            'solid_geometry': geo_obj.solid_geometry
-                        }
-                    })
-
-                    # detect if solid_geometry is empty and this require list flattening which is "heavy"
-                    # or just looking in the lists (they are one level depth) and if any is not empty
-                    # proceed with object creation, if there are empty and the number of them is the length
-                    # of the list then we have an empty solid_geometry which should raise a Custom Exception
-                    empty_cnt = 0
-                    if not isinstance(geo_obj.solid_geometry, list):
-                        geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                    for g in geo_obj.solid_geometry:
-                        if g:
-                            break
-                        else:
-                            empty_cnt += 1
-
-                    if empty_cnt == len(geo_obj.solid_geometry):
-                        raise ValidationError("Empty Geometry", None)
-                    else:
-                        fc_obj.inform.emit('[success] %s: %s' %
-                                            (_("Isolation geometry created"), geo_obj.options["name"]))
-                    geo_obj.multigeo = False
-
-                    # ############################################################
-                    # ########## AREA SUBTRACTION ################################
-                    # ############################################################
-                    if self.ui.except_cb.get_value():
-                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                        geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
-
-                # TODO: Do something if this is None. Offer changing name?
-                self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
-
     def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0):
         # isolation_geometry produces an envelope that is going on the left of the geometry
         # (the copper features). To leave the least amount of burrs on the features
@@ -1117,65 +491,6 @@ class GerberObject(FlatCAMObj, Gerber):
                 return 'fail'
         return geom
 
-    def area_subtraction(self, geo, subtractor_geo=None):
-        """
-        Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo
-
-        :param geo: target geometry from which to subtract
-        :param subtractor_geo: geometry that acts as subtractor
-        :return:
-        """
-        new_geometry = []
-        target_geo = geo
-
-        if subtractor_geo:
-            sub_union = cascaded_union(subtractor_geo)
-        else:
-            name = self.ui.obj_combo.currentText()
-            subtractor_obj = self.app.collection.get_by_name(name)
-            sub_union = cascaded_union(subtractor_obj.solid_geometry)
-
-        try:
-            for geo_elem in target_geo:
-                if isinstance(geo_elem, Polygon):
-                    for ring in self.poly2rings(geo_elem):
-                        new_geo = ring.difference(sub_union)
-                        if new_geo and not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-                elif isinstance(geo_elem, MultiPolygon):
-                    for poly in geo_elem:
-                        for ring in self.poly2rings(poly):
-                            new_geo = ring.difference(sub_union)
-                            if new_geo and not new_geo.is_empty:
-                                new_geometry.append(new_geo)
-                elif isinstance(geo_elem, LineString):
-                    new_geo = geo_elem.difference(sub_union)
-                    if new_geo:
-                        if not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-                elif isinstance(geo_elem, MultiLineString):
-                    for line_elem in geo_elem:
-                        new_geo = line_elem.difference(sub_union)
-                        if new_geo and not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-        except TypeError:
-            if isinstance(target_geo, Polygon):
-                for ring in self.poly2rings(target_geo):
-                    new_geo = ring.difference(sub_union)
-                    if new_geo:
-                        if not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-            elif isinstance(target_geo, LineString):
-                new_geo = target_geo.difference(sub_union)
-                if new_geo and not new_geo.is_empty:
-                    new_geometry.append(new_geo)
-            elif isinstance(target_geo, MultiLineString):
-                for line_elem in target_geo:
-                    new_geo = line_elem.difference(sub_union)
-                    if new_geo and not new_geo.is_empty:
-                        new_geometry.append(new_geo)
-        return new_geometry
-
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
             return

+ 3 - 3
AppTools/ToolEtchCompensation.py

@@ -95,7 +95,7 @@ class ToolEtchCompensation(AppTool):
 
         hlay_1 = QtWidgets.QHBoxLayout()
 
-        self.oz_entry = NumericalEvalEntry()
+        self.oz_entry = NumericalEvalEntry(border_color='#0069A9')
         self.oz_entry.setPlaceholderText(_("Oz value"))
         self.oz_to_um_entry = FCEntry()
         self.oz_to_um_entry.setPlaceholderText(_("Microns value"))
@@ -116,7 +116,7 @@ class ToolEtchCompensation(AppTool):
 
         hlay_2 = QtWidgets.QHBoxLayout()
 
-        self.mils_entry = NumericalEvalEntry()
+        self.mils_entry = NumericalEvalEntry(border_color='#0069A9')
         self.mils_entry.setPlaceholderText(_("Mils value"))
         self.mils_to_um_entry = FCEntry()
         self.mils_to_um_entry.setPlaceholderText(_("Microns value"))
@@ -180,7 +180,7 @@ class ToolEtchCompensation(AppTool):
             _("The ratio between depth etch and lateral etch .\n"
               "Accepts real numbers and formulas using the operators: /,*,+,-,%")
         )
-        self.factor_entry = NumericalEvalEntry()
+        self.factor_entry = NumericalEvalEntry(border_color='#0069A9')
         self.factor_entry.setPlaceholderText(_("Real number or formula"))
         self.factor_entry.setObjectName(_("Etch_factor"))
 

+ 626 - 101
AppTools/ToolIsolation.py

@@ -213,7 +213,7 @@ class ToolIsolation(AppTool, Gerber):
               "- 'V-shape'\n"
               "- Circular")
         )
-        self.tool_type_radio.setObjectName(_("Tool Type"))
+        self.tool_type_radio.setObjectName("i_tool_type")
 
         self.grid3.addWidget(self.tool_type_label, 2, 0)
         self.grid3.addWidget(self.tool_type_radio, 2, 1)
@@ -226,7 +226,7 @@ class ToolIsolation(AppTool, Gerber):
         self.tipdia_entry.set_precision(self.decimals)
         self.tipdia_entry.set_range(0.0000, 9999.9999)
         self.tipdia_entry.setSingleStep(0.1)
-        self.tipdia_entry.setObjectName(_("V-Tip Dia"))
+        self.tipdia_entry.setObjectName("i_vtipdia")
 
         self.grid3.addWidget(self.tipdialabel, 3, 0)
         self.grid3.addWidget(self.tipdia_entry, 3, 1)
@@ -240,7 +240,7 @@ class ToolIsolation(AppTool, Gerber):
         self.tipangle_entry.set_precision(self.decimals)
         self.tipangle_entry.set_range(0.0000, 180.0000)
         self.tipangle_entry.setSingleStep(5)
-        self.tipangle_entry.setObjectName(_("V-Tip Angle"))
+        self.tipangle_entry.setObjectName("i_vtipangle")
 
         self.grid3.addWidget(self.tipanglelabel, 4, 0)
         self.grid3.addWidget(self.tipangle_entry, 4, 1)
@@ -254,7 +254,7 @@ class ToolIsolation(AppTool, Gerber):
         self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_range(-99999.9999, 0.0000)
-        self.cutz_entry.setObjectName(_("Cut Z"))
+        self.cutz_entry.setObjectName("i_vcutz")
 
         self.grid3.addWidget(cutzlabel, 5, 0)
         self.grid3.addWidget(self.cutz_entry, 5, 1)
@@ -269,7 +269,7 @@ class ToolIsolation(AppTool, Gerber):
         self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message)
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.set_range(0.000, 9999.9999)
-        self.addtool_entry.setObjectName(_("Tool Dia"))
+        self.addtool_entry.setObjectName("i_new_tooldia")
 
         self.grid3.addWidget(self.addtool_entry_lbl, 6, 0)
         self.grid3.addWidget(self.addtool_entry, 6, 1)
@@ -402,7 +402,7 @@ class ToolIsolation(AppTool, Gerber):
         self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
                                         {'label': _('Ext'), 'value': 'ext'},
                                         {'label': _('Int'), 'value': 'int'}])
-        self.iso_type_radio.setObjectName("i_type")
+        self.iso_type_radio.setObjectName("i_iso_type")
 
         self.grid3.addWidget(self.iso_type_label, 17, 0)
         self.grid3.addWidget(self.iso_type_radio, 17, 1)
@@ -432,8 +432,8 @@ class ToolIsolation(AppTool, Gerber):
         self.grid3.addWidget(self.gen_param_label, 24, 0, 1, 2)
 
         # Rest Machining
-        self.rest_cb = FCCheckBox('%s' % _("Rest Machining"))
-        self.rest_cb.setObjectName("i_rest_machining")
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.rest_cb.setObjectName("i_rest")
         self.rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
               "Basically it will isolate outside PCB features,\n"
@@ -677,21 +677,21 @@ class ToolIsolation(AppTool, Gerber):
         self.tooldia = None
 
         self.form_fields = {
-            "tools_iso_passes": self.passes_entry,
-            "tools_iso_overlap": self.iso_overlap_entry,
-            "tools_iso_milling_type": self.milling_type_radio,
-            "tools_iso_combine": self.combine_passes_cb,
-            "tools_iso_follow": self.follow_cb,
-            "tools_iso_isotype": self.iso_type_radio
+            "tools_iso_passes":         self.passes_entry,
+            "tools_iso_overlap":        self.iso_overlap_entry,
+            "tools_iso_milling_type":   self.milling_type_radio,
+            "tools_iso_combine":        self.combine_passes_cb,
+            "tools_iso_follow":         self.follow_cb,
+            "tools_iso_isotype":        self.iso_type_radio
         }
 
         self.name2option = {
-            "i_passes": "tools_iso_passes",
-            "i_overlap": "tools_iso_overlap",
-            "i_milling_type": "tools_iso_milling_type",
-            "i_combine": "tools_iso_combine",
-            "i_follow": "tools_iso_follow",
-            "i_type": "tools_iso_isotype"
+            "i_passes":         "tools_iso_passes",
+            "i_overlap":        "tools_iso_overlap",
+            "i_milling_type":   "tools_iso_milling_type",
+            "i_combine":        "tools_iso_combine",
+            "i_follow":         "tools_iso_follow",
+            "i_iso_type":       "tools_iso_isotype"
         }
 
         self.old_tool_dia = None
@@ -715,7 +715,7 @@ class ToolIsolation(AppTool, Gerber):
 
         self.type_excobj_radio.activated_custom.connect(self.on_type_excobj_index_changed)
         self.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
-        self.addtool_from_db_btn.clicked.connect(self.on_ncc_tool_add_from_db_clicked)
+        self.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
 
         self.generate_iso_button.clicked.connect(self.on_isolate_click)
         self.reset_button.clicked.connect(self.set_tool_ui)
@@ -731,21 +731,6 @@ class ToolIsolation(AppTool, Gerber):
             "gerber": "Gerber", "geometry": "Geometry"
         }[self.type_excobj_radio.get_value()]
 
-    def on_operation_change(self, val):
-        if val == 'iso':
-            self.milling_type_label.setEnabled(True)
-            self.milling_type_radio.setEnabled(True)
-        else:
-            self.milling_type_label.setEnabled(False)
-            self.milling_type_radio.setEnabled(False)
-
-        current_row = self.tools_table.currentRow()
-        try:
-            current_uid = int(self.tools_table.item(current_row, 3).text())
-            self.iso_tools[current_uid]['data']['tools_nccoperation'] = val
-        except AttributeError:
-            return
-
     def on_row_selection_change(self):
         self.blockSignals(True)
 
@@ -782,7 +767,7 @@ class ToolIsolation(AppTool, Gerber):
                                     form_value_storage = tooluid_value[key]
                                     self.storage_to_form(form_value_storage)
                 except Exception as e:
-                    log.debug("NonCopperClear ---> update_ui() " + str(e))
+                    log.debug("ToolIsolation ---> update_ui() " + str(e))
             else:
                 self.tool_data_label.setText(
                     "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
@@ -797,7 +782,7 @@ class ToolIsolation(AppTool, Gerber):
                     try:
                         self.form_fields[form_key].set_value(dict_storage[form_key])
                     except Exception as e:
-                        log.debug("NonCopperClear.storage_to_form() --> %s" % str(e))
+                        log.debug("ToolIsolation.storage_to_form() --> %s" % str(e))
                         pass
 
     def form_to_storage(self):
@@ -831,7 +816,7 @@ class ToolIsolation(AppTool, Gerber):
     def on_apply_param_to_all_clicked(self):
         if self.tools_table.rowCount() == 0:
             # there is no tool in tool table so we can't save the GUI elements values to storage
-            log.debug("NonCopperClear.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.")
+            log.debug("ToolIsolation.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.")
             return
 
         self.blockSignals(True)
@@ -853,47 +838,7 @@ class ToolIsolation(AppTool, Gerber):
         for tooluid_key, tooluid_val in self.iso_tools.items():
             tooluid_val['data'] = deepcopy(temp_tool_data)
 
-        # store all the data associated with the row parameter to the self.tools storage
-        # tooldia_item = float(self.tools_table.item(row, 1).text())
-        # type_item = self.tools_table.cellWidget(row, 2).currentText()
-        # operation_type_item = self.tools_table.cellWidget(row, 4).currentText()
-        #
-        # nccoffset_item = self.ncc_choice_offset_cb.get_value()
-        # nccoffset_value_item = float(self.ncc_offset_spinner.get_value())
-
-        # this new dict will hold the actual useful data, another dict that is the value of key 'data'
-        # temp_tools = {}
-        # temp_dia = {}
-        # temp_data = {}
-        #
-        # for tooluid_key, tooluid_value in self.iso_tools.items():
-        #     for key, value in tooluid_value.items():
-        #         if key == 'data':
-        #             # update the 'data' section
-        #             for data_key in tooluid_value[key].keys():
-        #                 for form_key, form_value in self.form_fields.items():
-        #                     if form_key == data_key:
-        #                         temp_data[data_key] = form_value.get_value()
-        #                 # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
-        #                 # updated from self.app.defaults
-        #                 if data_key not in self.form_fields:
-        #                     temp_data[data_key] = value[data_key]
-        #             temp_dia[key] = deepcopy(temp_data)
-        #             temp_data.clear()
-        #
-        #         elif key == 'solid_geometry':
-        #             temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry'])
-        #         else:
-        #             temp_dia[key] = deepcopy(value)
-        #
-        #         temp_tools[tooluid_key] = deepcopy(temp_dia)
-        #
-        # self.iso_tools.clear()
-        # self.iso_tools = deepcopy(temp_tools)
-        # temp_tools.clear()
-
         self.app.inform.emit('[success] %s' % _("Current Tool parameters were applied to all tools."))
-
         self.blockSignals(False)
 
     def on_add_tool_by_key(self):
@@ -979,14 +924,14 @@ class ToolIsolation(AppTool, Gerber):
             self.iso_type_radio.set_value('full')
             self.iso_type_radio.hide()
 
-            self.follow_cb.setChecked(False)
+            self.follow_cb.set_value(False)
             self.follow_cb.hide()
             self.follow_label.hide()
 
-            self.rest_cb.setChecked(False)
+            self.rest_cb.set_value(False)
             self.rest_cb.hide()
 
-            self.except_cb.setChecked(False)
+            self.except_cb.set_value(False)
             self.except_cb.hide()
 
             self.select_combo.setCurrentIndex(0)
@@ -995,7 +940,6 @@ class ToolIsolation(AppTool, Gerber):
         else:
             self.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
 
-            # TODO remember to set the GUI elements to values from app.defaults dict
             self.tool_type_radio.set_value(self.app.defaults["tools_iso_tool_type"])
             self.tool_type_label.show()
             self.tool_type_radio.show()
@@ -1007,17 +951,17 @@ class ToolIsolation(AppTool, Gerber):
             self.iso_type_radio.set_value(self.app.defaults["tools_iso_isotype"])
             self.iso_type_radio.show()
 
-            self.follow_cb.setChecked(self.app.defaults["tools_iso_follow"])
+            self.follow_cb.set_value(self.app.defaults["tools_iso_follow"])
             self.follow_cb.show()
             self.follow_label.show()
 
-            self.rest_cb.setChecked(self.app.defaults["tools_iso_rest"])
+            self.rest_cb.set_value(self.app.defaults["tools_iso_rest"])
             self.rest_cb.show()
 
-            self.except_cb.setChecked(self.app.defaults["tools_iso_isoexcept"])
+            self.except_cb.set_value(self.app.defaults["tools_iso_isoexcept"])
             self.except_cb.show()
 
-            self.select_combo.setCurrentIndex(self.app.defaults["tools_iso_selection"])
+            self.select_combo.set_value(self.app.defaults["tools_iso_selection"])
             self.select_combo.show()
             self.select_label.show()
 
@@ -1185,21 +1129,12 @@ class ToolIsolation(AppTool, Gerber):
 
                     tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
 
-                    # operation_type = FCComboBox()
-                    # operation_type.addItems(['iso_op', 'clear_op'])
-                    #
-                    # # operation_type.setStyleSheet('background-color: rgb(255,255,255)')
-                    # op_idx = operation_type.findText(tooluid_value['operation'])
-                    # operation_type.setCurrentIndex(op_idx)
-
                     self.tools_table.setItem(row_no, 1, dia)  # Diameter
                     self.tools_table.setCellWidget(row_no, 2, tool_type_item)
 
                     # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
                     self.tools_table.setItem(row_no, 3, tool_uid_item)  # Tool unique ID
 
-                    # self.tools_table.setCellWidget(row_no, 4, operation_type)
-
         # make the diameter column editable
         for row in range(tool_id):
             self.tools_table.item(row, 1).setFlags(
@@ -1746,6 +1681,596 @@ class ToolIsolation(AppTool, Gerber):
                               isotooldia=self.iso_dia_list,
                               outname=self.o_name)
 
+
+
+
+    # ###########################################
+    # ###########################################
+    # ###########################################
+    # ###########################################
+
+    def on_iso_button_click(self, *args):
+
+        obj = self.app.collection.get_active()
+
+        self.iso_type = 2
+        if self.ui.iso_type_radio.get_value() == 'ext':
+            self.iso_type = 0
+        if self.ui.iso_type_radio.get_value() == 'int':
+            self.iso_type = 1
+
+        def worker_task(iso_obj, app_obj):
+            with self.app.proc_container.new(_("Isolating...")):
+                if self.ui.follow_cb.get_value() is True:
+                    iso_obj.follow_geo()
+                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
+                    iso_obj.ui.plot_cb.toggle()
+                else:
+                    app_obj.defaults.report_usage("gerber_on_iso_button")
+                    self.read_form()
+
+                    iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single'
+                    self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
+
+        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
+
+    def follow_geo(self, outname=None):
+        """
+        Creates a geometry object "following" the gerber paths.
+
+        :return: None
+        """
+
+        # default_name = self.options["name"] + "_follow"
+        # follow_name = outname or default_name
+
+        if outname is None:
+            follow_name = self.options["name"] + "_follow"
+        else:
+            follow_name = outname
+
+        def follow_init(follow_obj, app):
+            # Propagate options
+            follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
+            follow_obj.solid_geometry = self.follow_geometry
+
+        # TODO: Do something if this is None. Offer changing name?
+        try:
+            self.app.app_obj.new_object("geometry", follow_name, follow_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)
+
+    def isolate_handler(self, iso_type, iso_scope):
+
+        if iso_scope == 'all':
+            self.isolate(iso_type=iso_type)
+        else:
+            # 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_mouse_click_release)
+
+            if self.app.is_legacy is False:
+                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.mr)
+
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
+
+    def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
+                milling_type=None, follow=None, plot=True):
+        """
+        Creates an isolation routing geometry object in the project.
+
+        :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
+        :param geometry: specific geometry to isolate
+        :param dia: Tool diameter
+        :param passes: Number of tool widths to cut
+        :param overlap: Overlap between passes in fraction of tool diameter
+        :param outname: Base name of the output object
+        :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
+        :param milling_type: type of milling: conventional or climbing
+        :param follow: Boolean: if to generate a 'follow' geometry
+        :param plot: Boolean: if to plot the resulting geometry object
+        :return: None
+        """
+
+        if geometry is None:
+            work_geo = self.follow_geometry if follow is True else self.solid_geometry
+        else:
+            work_geo = geometry
+
+        if dia is None:
+            dia = float(self.options["isotooldia"])
+
+        if passes is None:
+            passes = int(self.options["isopasses"])
+
+        if overlap is None:
+            overlap = float(self.options["isooverlap"])
+
+        overlap /= 100.0
+
+        combine = self.options["combine_passes"] if combine is None else bool(combine)
+
+        if milling_type is None:
+            milling_type = self.options["milling_type"]
+
+        if iso_type is None:
+            iso_t = 2
+        else:
+            iso_t = iso_type
+
+        base_name = self.options["name"]
+
+        if combine:
+            if outname is None:
+                if self.iso_type == 0:
+                    iso_name = base_name + "_ext_iso"
+                elif self.iso_type == 1:
+                    iso_name = base_name + "_int_iso"
+                else:
+                    iso_name = base_name + "_iso"
+            else:
+                iso_name = outname
+
+            def iso_init(geo_obj, app_obj):
+                # Propagate options
+                geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
+                geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper()
+
+                geo_obj.solid_geometry = []
+
+                # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
+                if self.ui.tool_type_radio.get_value() == 'v':
+                    new_cutz = self.ui.cutz_spinner.get_value()
+                    new_vtipdia = self.ui.tipdia_spinner.get_value()
+                    new_vtipangle = self.ui.tipangle_spinner.get_value()
+                    tool_type = 'V'
+                else:
+                    new_cutz = self.app.defaults['geometry_cutz']
+                    new_vtipdia = self.app.defaults['geometry_vtipdia']
+                    new_vtipangle = self.app.defaults['geometry_vtipangle']
+                    tool_type = 'C1'
+
+                # store here the default data for Geometry Data
+                default_data = {}
+                default_data.update({
+                    "name": iso_name,
+                    "plot": self.app.defaults['geometry_plot'],
+                    "cutz": new_cutz,
+                    "vtipdia": new_vtipdia,
+                    "vtipangle": new_vtipangle,
+                    "travelz": self.app.defaults['geometry_travelz'],
+                    "feedrate": self.app.defaults['geometry_feedrate'],
+                    "feedrate_z": self.app.defaults['geometry_feedrate_z'],
+                    "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
+                    "dwell": self.app.defaults['geometry_dwell'],
+                    "dwelltime": self.app.defaults['geometry_dwelltime'],
+                    "multidepth": self.app.defaults['geometry_multidepth'],
+                    "ppname_g": self.app.defaults['geometry_ppname_g'],
+                    "depthperpass": self.app.defaults['geometry_depthperpass'],
+                    "extracut": self.app.defaults['geometry_extracut'],
+                    "extracut_length": self.app.defaults['geometry_extracut_length'],
+                    "toolchange": self.app.defaults['geometry_toolchange'],
+                    "toolchangez": self.app.defaults['geometry_toolchangez'],
+                    "endz": self.app.defaults['geometry_endz'],
+                    "spindlespeed": self.app.defaults['geometry_spindlespeed'],
+                    "toolchangexy": self.app.defaults['geometry_toolchangexy'],
+                    "startz": self.app.defaults['geometry_startz']
+                })
+
+                geo_obj.tools = {}
+                geo_obj.tools['1'] = {}
+                geo_obj.tools.update({
+                    '1': {
+                        'tooldia': float(self.options["isotooldia"]),
+                        'offset': 'Path',
+                        'offset_value': 0.0,
+                        'type': _('Rough'),
+                        'tool_type': tool_type,
+                        'data': default_data,
+                        'solid_geometry': geo_obj.solid_geometry
+                    }
+                })
+
+                for nr_pass in range(passes):
+                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
+
+                    # if milling type is climb then the move is counter-clockwise around features
+                    mill_dir = 1 if milling_type == 'cl' else 0
+                    geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
+                                                  follow=follow, nr_passes=nr_pass)
+
+                    if geom == 'fail':
+                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
+                        return 'fail'
+                    geo_obj.solid_geometry.append(geom)
+
+                    # update the geometry in the tools
+                    geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry
+
+                # detect if solid_geometry is empty and this require list flattening which is "heavy"
+                # or just looking in the lists (they are one level depth) and if any is not empty
+                # proceed with object creation, if there are empty and the number of them is the length
+                # of the list then we have an empty solid_geometry which should raise a Custom Exception
+                empty_cnt = 0
+                if not isinstance(geo_obj.solid_geometry, list) and \
+                        not isinstance(geo_obj.solid_geometry, MultiPolygon):
+                    geo_obj.solid_geometry = [geo_obj.solid_geometry]
+
+                for g in geo_obj.solid_geometry:
+                    if g:
+                        break
+                    else:
+                        empty_cnt += 1
+
+                if empty_cnt == len(geo_obj.solid_geometry):
+                    raise ValidationError("Empty Geometry", None)
+                else:
+                    app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
+
+                # even if combine is checked, one pass is still single-geo
+                geo_obj.multigeo = True if passes > 1 else False
+
+                # ############################################################
+                # ########## AREA SUBTRACTION ################################
+                # ############################################################
+                if self.ui.except_cb.get_value():
+                    self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
+                    geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
+
+            # TODO: Do something if this is None. Offer changing name?
+            self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
+        else:
+            for i in range(passes):
+                offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia)
+                if passes > 1:
+                    if outname is None:
+                        if self.iso_type == 0:
+                            iso_name = base_name + "_ext_iso" + str(i + 1)
+                        elif self.iso_type == 1:
+                            iso_name = base_name + "_int_iso" + str(i + 1)
+                        else:
+                            iso_name = base_name + "_iso" + str(i + 1)
+                    else:
+                        iso_name = outname
+                else:
+                    if outname is None:
+                        if self.iso_type == 0:
+                            iso_name = base_name + "_ext_iso"
+                        elif self.iso_type == 1:
+                            iso_name = base_name + "_int_iso"
+                        else:
+                            iso_name = base_name + "_iso"
+                    else:
+                        iso_name = outname
+
+                def iso_init(geo_obj, fc_obj):
+                    # Propagate options
+                    geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
+                    if self.ui.tool_type_radio.get_value() == 'v':
+                        geo_obj.tool_type = 'V'
+                    else:
+                        geo_obj.tool_type = 'C1'
+
+                    # if milling type is climb then the move is counter-clockwise around features
+                    mill_dir = 1 if milling_type == 'cl' else 0
+                    geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
+                                                  follow=follow,
+                                                  nr_passes=i)
+
+                    if geom == 'fail':
+                        fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
+                        return 'fail'
+
+                    geo_obj.solid_geometry = geom
+
+                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
+                    # even if the resulting geometry is not multigeo we add the tools dict which will hold the data
+                    # required to be transfered to the Geometry object
+                    if self.ui.tool_type_radio.get_value() == 'v':
+                        new_cutz = self.ui.cutz_spinner.get_value()
+                        new_vtipdia = self.ui.tipdia_spinner.get_value()
+                        new_vtipangle = self.ui.tipangle_spinner.get_value()
+                        tool_type = 'V'
+                    else:
+                        new_cutz = self.app.defaults['geometry_cutz']
+                        new_vtipdia = self.app.defaults['geometry_vtipdia']
+                        new_vtipangle = self.app.defaults['geometry_vtipangle']
+                        tool_type = 'C1'
+
+                    # store here the default data for Geometry Data
+                    default_data = {}
+                    default_data.update({
+                        "name": iso_name,
+                        "plot": self.app.defaults['geometry_plot'],
+                        "cutz": new_cutz,
+                        "vtipdia": new_vtipdia,
+                        "vtipangle": new_vtipangle,
+                        "travelz": self.app.defaults['geometry_travelz'],
+                        "feedrate": self.app.defaults['geometry_feedrate'],
+                        "feedrate_z": self.app.defaults['geometry_feedrate_z'],
+                        "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
+                        "dwell": self.app.defaults['geometry_dwell'],
+                        "dwelltime": self.app.defaults['geometry_dwelltime'],
+                        "multidepth": self.app.defaults['geometry_multidepth'],
+                        "ppname_g": self.app.defaults['geometry_ppname_g'],
+                        "depthperpass": self.app.defaults['geometry_depthperpass'],
+                        "extracut": self.app.defaults['geometry_extracut'],
+                        "extracut_length": self.app.defaults['geometry_extracut_length'],
+                        "toolchange": self.app.defaults['geometry_toolchange'],
+                        "toolchangez": self.app.defaults['geometry_toolchangez'],
+                        "endz": self.app.defaults['geometry_endz'],
+                        "spindlespeed": self.app.defaults['geometry_spindlespeed'],
+                        "toolchangexy": self.app.defaults['geometry_toolchangexy'],
+                        "startz": self.app.defaults['geometry_startz']
+                    })
+
+                    geo_obj.tools = {}
+                    geo_obj.tools['1'] = {}
+                    geo_obj.tools.update({
+                        '1': {
+                            'tooldia': float(self.options["isotooldia"]),
+                            'offset': 'Path',
+                            'offset_value': 0.0,
+                            'type': _('Rough'),
+                            'tool_type': tool_type,
+                            'data': default_data,
+                            'solid_geometry': geo_obj.solid_geometry
+                        }
+                    })
+
+                    # detect if solid_geometry is empty and this require list flattening which is "heavy"
+                    # or just looking in the lists (they are one level depth) and if any is not empty
+                    # proceed with object creation, if there are empty and the number of them is the length
+                    # of the list then we have an empty solid_geometry which should raise a Custom Exception
+                    empty_cnt = 0
+                    if not isinstance(geo_obj.solid_geometry, list):
+                        geo_obj.solid_geometry = [geo_obj.solid_geometry]
+
+                    for g in geo_obj.solid_geometry:
+                        if g:
+                            break
+                        else:
+                            empty_cnt += 1
+
+                    if empty_cnt == len(geo_obj.solid_geometry):
+                        raise ValidationError("Empty Geometry", None)
+                    else:
+                        fc_obj.inform.emit('[success] %s: %s' %
+                                            (_("Isolation geometry created"), geo_obj.options["name"]))
+                    geo_obj.multigeo = False
+
+                    # ############################################################
+                    # ########## AREA SUBTRACTION ################################
+                    # ############################################################
+                    if self.ui.except_cb.get_value():
+                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
+                        geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
+
+                # TODO: Do something if this is None. Offer changing name?
+                self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
+
+    def area_subtraction(self, geo, subtractor_geo=None):
+        """
+        Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo
+
+        :param geo: target geometry from which to subtract
+        :param subtractor_geo: geometry that acts as subtractor
+        :return:
+        """
+        new_geometry = []
+        target_geo = geo
+
+        if subtractor_geo:
+            sub_union = cascaded_union(subtractor_geo)
+        else:
+            name = self.ui.obj_combo.currentText()
+            subtractor_obj = self.app.collection.get_by_name(name)
+            sub_union = cascaded_union(subtractor_obj.solid_geometry)
+
+        try:
+            for geo_elem in target_geo:
+                if isinstance(geo_elem, Polygon):
+                    for ring in self.poly2rings(geo_elem):
+                        new_geo = ring.difference(sub_union)
+                        if new_geo and not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+                elif isinstance(geo_elem, MultiPolygon):
+                    for poly in geo_elem:
+                        for ring in self.poly2rings(poly):
+                            new_geo = ring.difference(sub_union)
+                            if new_geo and not new_geo.is_empty:
+                                new_geometry.append(new_geo)
+                elif isinstance(geo_elem, LineString):
+                    new_geo = geo_elem.difference(sub_union)
+                    if new_geo:
+                        if not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+                elif isinstance(geo_elem, MultiLineString):
+                    for line_elem in geo_elem:
+                        new_geo = line_elem.difference(sub_union)
+                        if new_geo and not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+        except TypeError:
+            if isinstance(target_geo, Polygon):
+                for ring in self.poly2rings(target_geo):
+                    new_geo = ring.difference(sub_union)
+                    if new_geo:
+                        if not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+            elif isinstance(target_geo, LineString):
+                new_geo = target_geo.difference(sub_union)
+                if new_geo and not new_geo.is_empty:
+                    new_geometry.append(new_geo)
+            elif isinstance(target_geo, MultiLineString):
+                for line_elem in target_geo:
+                    new_geo = line_elem.difference(sub_union)
+                    if new_geo and not new_geo.is_empty:
+                        new_geometry.append(new_geo)
+        return new_geometry
+
+    def on_mouse_click_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            right_button = 2
+            self.app.event_is_dragging = self.app.event_is_dragging
+        else:
+            event_pos = (event.xdata, event.ydata)
+            right_button = 3
+            self.app.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)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+        else:
+            curr_pos = (curr_pos[0], curr_pos[1])
+
+        if event.button == 1:
+            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]))
+
+            if self.app.selection_type is not None:
+                self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
+                self.app.selection_type = None
+            elif clicked_poly:
+                if clicked_poly not in self.poly_dict.values():
+                    shape_id = self.app.tool_shapes.add(tolerance=self.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 isolation."))
+                    )
+                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 isolation."))
+                    )
+
+                self.app.tool_shapes.redraw()
+            else:
+                self.app.inform.emit(_("No polygon detected under click position."))
+        elif event.button == right_button and self.app.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_mouse_click_release)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+
+            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.isolate(iso_type=self.iso_type, geometry=poly_list)
+                self.poly_dict.clear()
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
+
+    def selection_area_handler(self, start_pos, end_pos, sel_type):
+        """
+        :param start_pos: mouse position when the selection LMB click was done
+        :param end_pos: mouse position when the left mouse button is released
+        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :return:
+        """
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+
+        # delete previous selection shape
+        self.app.delete_selection_shape()
+
+        added_poly_count = 0
+        try:
+            for geo in self.solid_geometry:
+                if geo not in self.poly_dict.values():
+                    if sel_type is True:
+                        if geo.within(poly_selection):
+                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                                shape=geo,
+                                                                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] = geo
+                            added_poly_count += 1
+                    else:
+                        if poly_selection.intersects(geo):
+                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                                shape=geo,
+                                                                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] = geo
+                            added_poly_count += 1
+        except TypeError:
+            if self.solid_geometry not in self.poly_dict.values():
+                if sel_type is True:
+                    if self.solid_geometry.within(poly_selection):
+                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                            shape=self.solid_geometry,
+                                                            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] = self.solid_geometry
+                        added_poly_count += 1
+                else:
+                    if poly_selection.intersects(self.solid_geometry):
+                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                            shape=self.solid_geometry,
+                                                            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] = self.solid_geometry
+                        added_poly_count += 1
+
+        if added_poly_count > 0:
+            self.app.tool_shapes.redraw()
+            self.app.inform.emit(
+                '%s: %d. %s' % (_("Added polygon"),
+                                int(added_poly_count),
+                                _("Click to add next polygon or right click to start isolation."))
+            )
+        else:
+            self.app.inform.emit(_("No polygon in selection."))
+
+    # ###########################################
+    # ###########################################
+    # ###########################################
+    # ###########################################
+
+
+
+
     # To be called after clicking on the plot.
     def on_mouse_release(self, event):
         if self.app.is_legacy is False:
@@ -2802,14 +3327,14 @@ class ToolIsolation(AppTool, Gerber):
                 return 'fail'
         return geom
 
-    def on_ncc_tool_add_from_db_executed(self, tool):
+    def on_tool_add_from_db_executed(self, tool):
         """
         Here add the tool from DB  in the selected geometry object
         :return:
         """
         tool_from_db = deepcopy(tool)
 
-        res = self.on_ncc_tool_from_db_inserted(tool=tool_from_db)
+        res = self.on_tool_from_db_inserted(tool=tool_from_db)
 
         for idx in range(self.app.ui.plot_tab_area.count()):
             if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
@@ -2828,7 +3353,7 @@ class ToolIsolation(AppTool, Gerber):
                 self.tools_table.selectRow(row)
         self.on_row_selection_change()
 
-    def on_ncc_tool_from_db_inserted(self, tool):
+    def on_tool_from_db_inserted(self, tool):
         """
         Called from the Tools DB object through a App method when adding a tool from Tools Database
         :param tool: a dict with the tool data
@@ -2887,7 +3412,7 @@ class ToolIsolation(AppTool, Gerber):
         # if self.tools_table.rowCount() != 0:
         #     self.param_frame.setDisabled(False)
 
-    def on_ncc_tool_add_from_db_clicked(self):
+    def on_tool_add_from_db_clicked(self):
         """
         Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
         and display the Tools Database tab in the form needed for the Tool adding
@@ -2917,7 +3442,7 @@ class ToolIsolation(AppTool, Gerber):
         self.cursor_pos = None
         self.mouse_is_dragging = False
 
-        prog_plot = True if self.app.defaults["tools_ncc_plotting"] == 'progressive' else False
+        prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False
         if prog_plot:
             self.temp_shapes.clear(update=True)
 

+ 1 - 1
AppTools/ToolNCC.py

@@ -509,7 +509,7 @@ class NonCopperClear(AppTool, Gerber):
         self.grid3.addWidget(self.gen_param_label, 24, 0, 1, 2)
 
         # Rest Machining
-        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest Machining"))
+        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest"))
         self.ncc_rest_cb.setObjectName("n_rest_machining")
 
         self.ncc_rest_cb.setToolTip(

+ 1 - 1
AppTools/ToolPaint.py

@@ -438,7 +438,7 @@ class ToolPaint(AppTool, Gerber):
         )
         grid4.addWidget(self.gen_param_label, 15, 0, 1, 2)
 
-        self.rest_cb = FCCheckBox('%s' % _("Rest Machining"))
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
         self.rest_cb.setObjectName('p_rest_machining')
         self.rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"

+ 4 - 0
CHANGELOG.md

@@ -14,6 +14,10 @@ CHANGELOG for FlatCAM beta
 - if there is a Gerber object selected then in Isolation Tool the Gerber object combobox will show that object name as current
 - made the Project Tree items not editable by clicking on selected Tree items (the object rename can still be done in the Selected tab)
 - working on Isolation Tool: added a Preferences section in Edit -> Preferences and updated their usage within the Isolation tool
+- fixed milling drills not plotting the resulting Geometry object
+- all tuple entries in the Preferences UI are now protected against letter entry
+- all entries in the Preferences UI that have numerical entry are protected now against letters
+- cleaned the Preferences UI in the Gerber area
 
 25.05.2020
 

+ 1 - 1
camlib.py

@@ -4374,7 +4374,7 @@ class CNCjob(Geometry):
 
                     self.gcode += self.create_gcode_multi_pass(geo, current_tooldia, extracut, self.extracut_length,
                                                                tolerance,
-                                                               z_move=z_move,_postproc=p,
+                                                               z_move=z_move, postproc=p,
                                                                old_point=current_pt)
 
                 # calculate the travel distance

+ 6 - 5
defaults.py

@@ -395,11 +395,12 @@ class FlatCAMDefaults:
         "tools_iso_follow": False,
         "tools_iso_isotype": "full",
 
-        "tools_iso_rest": False,
+        "tools_iso_rest":           False,
         "tools_iso_combine_passes": False,
-        "tools_iso_isoexcept": False,
-        "tools_iso_selection": _("All"),
-        "tools_iso_area_shape": "square",
+        "tools_iso_isoexcept":      False,
+        "tools_iso_selection":      _("All"),
+        "tools_iso_area_shape":     "square",
+        "tools_iso_plotting":       'normal',
 
         # NCC Tool
         "tools_ncctools": "1.0, 0.5",
@@ -415,13 +416,13 @@ class FlatCAMDefaults:
         "tools_ncc_offset_value": 0.0000,
         "tools_nccref": _('Itself'),
         "tools_ncc_area_shape": "square",
-        "tools_ncc_plotting": 'normal',
         "tools_nccmilling_type": 'cl',
         "tools_ncctool_type": 'C1',
         "tools_ncccutz": -0.05,
         "tools_ncctipdia": 0.1,
         "tools_ncctipangle": 30,
         "tools_nccnewdia": 0.1,
+        "tools_ncc_plotting": 'normal',
 
         # Cutout Tool
         "tools_cutouttooldia": 2.4,