Explorar el Código

Merged marius_stanciu/flatcam_beta/Beta into Beta
Work in progress in the Excellon Editor - probable crashes as things are in limbo.

Marius Stanciu hace 6 años
padre
commit
bdc3bfe854

+ 153 - 135
FlatCAMApp.py

@@ -6,7 +6,9 @@
 # MIT Licence                                               #
 # ###########################################################
 
-import urllib.request, urllib.parse, urllib.error
+import urllib.request
+import urllib.parse
+import urllib.error
 import getopt
 import random
 import simplejson as json
@@ -19,7 +21,6 @@ import subprocess
 import tkinter as tk
 from PyQt5 import QtPrintSupport
 
-import urllib.request, urllib.parse, urllib.error
 from contextlib import contextmanager
 import gc
 
@@ -96,8 +97,8 @@ class App(QtCore.QObject):
     # ####################################
     # Version and VERSION DATE ###########
     # ####################################
-    version = 8.93
-    version_date = "2019/08/10"
+    version = 8.94
+    version_date = "2019/08/31"
     beta = True
 
     # current date now
@@ -221,7 +222,7 @@ class App(QtCore.QObject):
             os.makedirs(os.path.join(self.data_path, 'postprocessors'))
             App.log.debug('Created data postprocessors folder: ' + os.path.join(self.data_path, 'postprocessors'))
 
-        self.postprocessorpaths = os.path.join(self.data_path,'postprocessors')
+        self.postprocessorpaths = os.path.join(self.data_path, 'postprocessors')
         if not os.path.exists(self.postprocessorpaths):
             os.makedirs(self.postprocessorpaths)
             App.log.debug('Created postprocessors folder: ' + self.postprocessorpaths)
@@ -295,7 +296,7 @@ class App(QtCore.QObject):
 
         QtCore.QObject.__init__(self)
         self.ui = FlatCAMGUI(self.version, self.beta, self)
-        self.set_ui_title(name="New Project")
+        self.set_ui_title(name=_("New Project - Not saved"))
 
         self.ui.geom_update[int, int, int, int, int].connect(self.save_geometry)
         self.ui.final_save.connect(self.final_save)
@@ -362,6 +363,7 @@ class App(QtCore.QObject):
             "global_layout": self.ui.general_defaults_form.general_gui_set_group.layout_combo,
             "global_hover": self.ui.general_defaults_form.general_gui_set_group.hover_cb,
             "global_selection_shape": self.ui.general_defaults_form.general_gui_set_group.selection_cb,
+
             # Gerber General
             "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
             "gerber_solid": self.ui.gerber_defaults_form.gerber_gen_group.solid_cb,
@@ -397,10 +399,14 @@ class App(QtCore.QObject):
             # Excellon General
             "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
             "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
-            "excellon_format_upper_in": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
-            "excellon_format_lower_in": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_in_entry,
-            "excellon_format_upper_mm": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_mm_entry,
-            "excellon_format_lower_mm": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_mm_entry,
+            "excellon_format_upper_in":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
+            "excellon_format_lower_in":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_in_entry,
+            "excellon_format_upper_mm":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_mm_entry,
+            "excellon_format_lower_mm":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_mm_entry,
             "excellon_zeros": self.ui.excellon_defaults_form.excellon_gen_group.excellon_zeros_radio,
             "excellon_units": self.ui.excellon_defaults_form.excellon_gen_group.excellon_units_radio,
             "excellon_optimization_type": self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio,
@@ -513,6 +519,8 @@ class App(QtCore.QObject):
             "tools_nccconnect": self.ui.tools_defaults_form.tools_ncc_group.ncc_connect_cb,
             "tools_ncccontour": self.ui.tools_defaults_form.tools_ncc_group.ncc_contour_cb,
             "tools_nccrest": self.ui.tools_defaults_form.tools_ncc_group.ncc_rest_cb,
+            "tools_ncc_offset_choice": self.ui.tools_defaults_form.tools_ncc_group.ncc_choice_offset_cb,
+            "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.reference_radio,
 
             # CutOut Tool
@@ -590,7 +598,8 @@ class App(QtCore.QObject):
             "tools_solderpaste_dwellfwd": self.ui.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry,
             "tools_solderpaste_speedrev": self.ui.tools_defaults_form.tools_solderpaste_group.speedrev_entry,
             "tools_solderpaste_dwellrev": self.ui.tools_defaults_form.tools_solderpaste_group.dwellrev_entry,
-            "tools_solderpaste_pp": self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo
+            "tools_solderpaste_pp": self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo,
+            "tools_sub_close_paths": self.ui.tools_defaults_form.tools_sub_group.close_paths_cb
 
         }
 
@@ -684,6 +693,7 @@ class App(QtCore.QObject):
             "global_def_win_w": 1024,
             "global_def_win_h": 650,
             "global_def_notebook_width": 1,
+
             # Constants...
             "global_defaults_save_period_ms": 20000,  # Time between default saves.
             "global_shell_shape": [500, 300],  # Shape of the shell in pixels.
@@ -702,6 +712,7 @@ class App(QtCore.QObject):
             "global_hover": False,
             "global_selection_shape": True,
             "global_layout": "compact",
+
             # Gerber General
             "gerber_plot": True,
             "gerber_solid": True,
@@ -853,6 +864,8 @@ class App(QtCore.QObject):
             "tools_nccconnect": True,
             "tools_ncccontour": True,
             "tools_nccrest": False,
+            "tools_ncc_offset_choice": False,
+            "tools_ncc_offset_value": 0.0000,
             "tools_nccref": 'itself',
 
             "tools_cutouttooldia": 0.00393701,
@@ -922,7 +935,9 @@ class App(QtCore.QObject):
             "tools_solderpaste_dwellfwd": 1,
             "tools_solderpaste_speedrev": 10,
             "tools_solderpaste_dwellrev": 1,
-            "tools_solderpaste_pp": 'Paste_1'
+            "tools_solderpaste_pp": 'Paste_1',
+
+            "tools_sub_close_paths": True
         })
 
         # ##############################
@@ -1513,7 +1528,6 @@ class App(QtCore.QObject):
         self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
             lambda: self.on_toggle_units(no_pref=False))
 
-
         # ##############################
         # ### GUI PREFERENCES SIGNALS ##
         # ##############################
@@ -2084,7 +2098,7 @@ class App(QtCore.QObject):
                                 ('BETA' if self.beta else ''),
                                 platform.architecture()[0],
                                 name)
-                            )
+                               )
 
     def defaults_read_form(self):
         for option in self.defaults_form_fields:
@@ -2514,7 +2528,7 @@ class App(QtCore.QObject):
         :param show: Opens the shell.
         :param error: Shows the message as an error.
         :param warning: Shows the message as an warning.
-        :param warning: Shows the message as an success.
+        :param success: Shows the message as an success.
         :param selected: Indicate that something was selected on canvas
         :return: None
         """
@@ -3504,6 +3518,10 @@ class App(QtCore.QObject):
         settings.setValue('saved_gui_state', self.ui.saveState())
         settings.setValue('maximized_gui', self.ui.isMaximized())
         settings.setValue('language', self.ui.general_defaults_form.general_app_group.language_cb.get_value())
+        settings.setValue('notebook_font_size',
+                          self.ui.general_defaults_form.general_gui_set_group.notebook_font_size_spinner.get_value())
+        settings.setValue('axis_font_size',
+                          self.ui.general_defaults_form.general_gui_set_group.axis_font_size_spinner.get_value())
 
         # This will write the setting to the platform specific storage.
         del settings
@@ -4150,8 +4168,9 @@ class App(QtCore.QObject):
 
     # Setting plot colors handlers
     def on_pf_color_entry(self):
-        self.defaults['global_plot_fill'] = self.ui.general_defaults_form.general_gui_group.pf_color_entry.get_value()[:7] + \
-                                            self.defaults['global_plot_fill'][7:9]
+        self.defaults['global_plot_fill'] = \
+            self.ui.general_defaults_form.general_gui_group.pf_color_entry.get_value()[:7] + \
+            self.defaults['global_plot_fill'][7:9]
         self.ui.general_defaults_form.general_gui_group.pf_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_plot_fill'])[:7])
 
@@ -4174,18 +4193,21 @@ class App(QtCore.QObject):
     def on_pf_color_spinner(self):
         spinner_value = self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.value()
         self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_plot_fill'] = self.defaults['global_plot_fill'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_plot_line'] = self.defaults['global_plot_line'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_plot_fill'] = \
+            self.defaults['global_plot_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_plot_line'] = \
+            self.defaults['global_plot_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
 
     def on_pf_color_slider(self):
         slider_value = self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value()
         self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.setValue(slider_value)
 
     def on_pl_color_entry(self):
-        self.defaults['global_plot_line'] = self.ui.general_defaults_form.general_gui_group.pl_color_entry.get_value()[:7] + \
-                                            self.defaults['global_plot_line'][7:9]
+        self.defaults['global_plot_line'] = \
+            self.ui.general_defaults_form.general_gui_group.pl_color_entry.get_value()[:7] + \
+            self.defaults['global_plot_line'][7:9]
         self.ui.general_defaults_form.general_gui_group.pl_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_plot_line'])[:7])
 
@@ -4208,8 +4230,9 @@ class App(QtCore.QObject):
 
     # Setting selection colors (left - right) handlers
     def on_sf_color_entry(self):
-        self.defaults['global_sel_fill'] = self.ui.general_defaults_form.general_gui_group.sf_color_entry.get_value()[:7] + \
-                                            self.defaults['global_sel_fill'][7:9]
+        self.defaults['global_sel_fill'] = \
+            self.ui.general_defaults_form.general_gui_group.sf_color_entry.get_value()[:7] + \
+            self.defaults['global_sel_fill'][7:9]
         self.ui.general_defaults_form.general_gui_group.sf_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_sel_fill'])[:7])
 
@@ -4232,18 +4255,21 @@ class App(QtCore.QObject):
     def on_sf_color_spinner(self):
         spinner_value = self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.value()
         self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_sel_fill'] = self.defaults['global_sel_fill'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_sel_line'] = self.defaults['global_sel_line'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_sel_fill'] = \
+            self.defaults['global_sel_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_sel_line'] = \
+            self.defaults['global_sel_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
 
     def on_sf_color_slider(self):
         slider_value = self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.value()
         self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.setValue(slider_value)
 
     def on_sl_color_entry(self):
-        self.defaults['global_sel_line'] = self.ui.general_defaults_form.general_gui_group.sl_color_entry.get_value()[:7] + \
-                                            self.defaults['global_sel_line'][7:9]
+        self.defaults['global_sel_line'] = \
+            self.ui.general_defaults_form.general_gui_group.sl_color_entry.get_value()[:7] + \
+            self.defaults['global_sel_line'][7:9]
         self.ui.general_defaults_form.general_gui_group.sl_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
 
@@ -4289,10 +4315,12 @@ class App(QtCore.QObject):
     def on_alt_sf_color_spinner(self):
         spinner_value = self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.value()
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_alt_sel_fill'] = self.defaults['global_alt_sel_fill'][:7] + \
-                                               (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_alt_sel_line'] = self.defaults['global_alt_sel_line'][:7] + \
-                                               (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_alt_sel_fill'] = \
+            self.defaults['global_alt_sel_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_alt_sel_line'] = \
+            self.defaults['global_alt_sel_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
 
     def on_alt_sf_color_slider(self):
         slider_value = self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.value()
@@ -4599,6 +4627,18 @@ class App(QtCore.QObject):
         # Re-fresh project options
         self.on_options_app2project()
 
+        # save the notebook font size
+        settings = QSettings("Open Source", "FlatCAM")
+        fsize = self.ui.general_defaults_form.general_gui_set_group.notebook_font_size_spinner.get_value()
+        settings.setValue('notebook_font_size', fsize)
+
+        # save the axis font size
+        g_fsize = self.ui.general_defaults_form.general_gui_set_group.axis_font_size_spinner.get_value()
+        settings.setValue('axis_font_size', g_fsize)
+
+        # This will write the setting to the platform specific storage.
+        del settings
+
     def handlePrint(self):
         self.report_usage("handlePrint()")
 
@@ -4813,7 +4853,6 @@ class App(QtCore.QObject):
                     self.inform.emit(
                         _("[WARNING_NOTCL] Adding Tool cancelled ..."))
 
-
     # It's meant to delete tools in tool tables via a 'Delete' shortcut key but only if certain conditions are met
     # See description bellow.
     def on_delete_keypress(self):
@@ -4861,7 +4900,7 @@ class App(QtCore.QObject):
             if self.collection.get_active():
                 self.log.debug("App.on_delete()")
 
-                while (self.collection.get_active()):
+                while self.collection.get_active():
                     obj_active = self.collection.get_active()
                     # if the deleted object is FlatCAMGerber then make sure to delete the possible mark shapes
                     if isinstance(obj_active, FlatCAMGerber):
@@ -4869,6 +4908,12 @@ class App(QtCore.QObject):
                             obj_active.mark_shapes[el].clear(update=True)
                             obj_active.mark_shapes[el].enabled = False
                             obj_active.mark_shapes[el] = None
+                    elif isinstance(obj_active, FlatCAMCNCjob):
+                        try:
+                            obj_active.annotation.clear(update=True)
+                            obj_active.annotation.enabled = False
+                        except AttributeError:
+                            pass
                     self.delete_first_selected()
 
                 self.inform.emit(_("Object(s) deleted ..."))
@@ -4986,9 +5031,9 @@ class App(QtCore.QObject):
             try:
                 if isinstance(obj, FlatCAMExcellon):
                     self.new_object("excellon", str(obj_name) + "_copy", initialize_excellon)
-                elif isinstance(obj,FlatCAMGerber):
+                elif isinstance(obj, FlatCAMGerber):
                     self.new_object("gerber", str(obj_name) + "_copy", initialize)
-                elif isinstance(obj,FlatCAMGeometry):
+                elif isinstance(obj, FlatCAMGeometry):
                     self.new_object("geometry", str(obj_name) + "_copy", initialize)
             except Exception as e:
                 return "Operation failed: %s" % str(e)
@@ -5030,9 +5075,9 @@ class App(QtCore.QObject):
             try:
                 if isinstance(obj, FlatCAMExcellon):
                     self.new_object("excellon", str(obj_name) + custom_name, initialize_excellon)
-                elif isinstance(obj,FlatCAMGerber):
+                elif isinstance(obj, FlatCAMGerber):
                     self.new_object("gerber", str(obj_name) + custom_name, initialize_gerber)
-                elif isinstance(obj,FlatCAMGeometry):
+                elif isinstance(obj, FlatCAMGeometry):
                     self.new_object("geometry", str(obj_name) + custom_name, initialize_geometry)
             except Exception as e:
                 return "Operation failed: %s" % str(e)
@@ -5167,8 +5212,8 @@ class App(QtCore.QObject):
                 return "Operation failed: %s" % str(e)
 
     def on_set_zero_click(self, event):
-        #this function will be available only for mouse left click
-        pos =[]
+        # this function will be available only for mouse left click
+        pos = []
         pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
         if event.button == 1:
             if self.grid_status() == True:
@@ -5179,7 +5224,7 @@ class App(QtCore.QObject):
             x = 0 - pos[0]
             y = 0 - pos[1]
             for obj in self.collection.get_list():
-                obj.offset((x,y))
+                obj.offset((x, y))
                 self.object_changed.emit(obj)
                 obj.plot()
                 # Update the object bounding box options
@@ -5322,7 +5367,7 @@ class App(QtCore.QObject):
 
             if response == bt_yes:
                 self.on_save_button()
-                self.inform.emit(_("[success] Defaults saved."))
+                self.inform.emit(_("[success] Preferences saved."))
             else:
                 self.preferences_changed_flag = False
                 return
@@ -5555,9 +5600,9 @@ class App(QtCore.QObject):
 
     def grid_status(self):
         if self.ui.grid_snap_btn.isChecked():
-            return 1
+            return True
         else:
-            return 0
+            return False
 
     def populate_cmenu_grids(self):
         units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
@@ -5567,7 +5612,7 @@ class App(QtCore.QObject):
 
         grid_toggle = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), _("Grid On/Off"))
         grid_toggle.setCheckable(True)
-        if self.grid_status():
+        if self.grid_status() == True:
             grid_toggle.setChecked(True)
         else:
             grid_toggle.setChecked(False)
@@ -5701,12 +5746,13 @@ class App(QtCore.QObject):
         self.plotcanvas.vispy_canvas.view.camera.pan_button_setting = self.defaults['global_pan_button']
 
         self.pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-        self.pos = (self.pos_canvas[0], self.pos_canvas[1])
-        self.app_cursor.enabled = False
 
-        if self.grid_status():
+        if self.grid_status() == True:
             self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1])
             self.app_cursor.enabled = True
+        else:
+            self.pos = (self.pos_canvas[0], self.pos_canvas[1])
+            self.app_cursor.enabled = False
 
         try:
             modifiers = QtWidgets.QApplication.keyboardModifiers()
@@ -5831,26 +5877,20 @@ class App(QtCore.QObject):
         :param event: contains information about the event.
         :return:
         """
-
+        pos = 0, 0
         pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-        if self.grid_status():
+        if self.grid_status() == True:
             pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = (pos_canvas[0], pos_canvas[1])
 
         # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
         # canvas menu
-        try:
-            if event.button == 2:  # right click
-                if self.ui.popMenu.mouse_is_panning is False:
-
-                    self.cursor = QtGui.QCursor()
-                    self.populate_cmenu_grids()
-                    self.ui.popMenu.popup(self.cursor.pos())
-
-        except Exception as e:
-            log.warning("Error: %s" % str(e))
-            return
+        if event.button == 2:  # right click
+            if self.ui.popMenu.mouse_is_panning is False:
+                self.cursor = QtGui.QCursor()
+                self.populate_cmenu_grids()
+                self.ui.popMenu.popup(self.cursor.pos())
 
         # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
         # selection and then select a type of selection ("enclosing" or "touching")
@@ -5866,7 +5906,6 @@ class App(QtCore.QObject):
                         # delete the selection shape(S) as it may be in the way
                         self.delete_selection_shape()
                         self.delete_hover_shape()
-
                 else:
                     if self.selection_type is not None:
                         self.selection_area_handler(self.pos, pos, self.selection_type)
@@ -5971,7 +6010,7 @@ class App(QtCore.QObject):
             else:
                 # case when there is only an object under the click and we toggle it
                 if len(objects_under_the_click_list) == 1:
-                    if self.collection.get_active() is None :
+                    if self.collection.get_active() is None:
                         self.collection.set_active(objects_under_the_click_list[0])
                         # create the selection box around the selected object
                         curr_sel_obj = self.collection.get_active()
@@ -6143,8 +6182,12 @@ class App(QtCore.QObject):
             face = Color(self.defaults['global_sel_fill'], alpha=0.2)
             outline = Color(self.defaults['global_sel_line'], alpha=0.8)
 
-        self.sel_objects_list.append(self.move_tool.sel_shapes.add(sel_rect, color=outline,
-                                                               face_color=face, update=True, layer=0, tolerance=None))
+        self.sel_objects_list.append(self.move_tool.sel_shapes.add(sel_rect,
+                                                                   color=outline,
+                                                                   face_color=face,
+                                                                   update=True,
+                                                                   layer=0,
+                                                                   tolerance=None))
 
     def draw_moving_selection_shape(self, old_coords, coords, **kwargs):
         """
@@ -6187,8 +6230,8 @@ class App(QtCore.QObject):
             msgbox = QtWidgets.QMessageBox()
             # msgbox.setText("<B>Save changes ...</B>")
             msgbox.setText(_("There are files/objects opened in FlatCAM.\n"
-                           "Creating a New project will delete them.\n"
-                           "Do you want to Save the project?"))
+                             "Creating a New project will delete them.\n"
+                             "Do you want to Save the project?"))
             msgbox.setWindowTitle(_("Save changes"))
             msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
             bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
@@ -6282,8 +6325,7 @@ class App(QtCore.QObject):
         # take the focus of the Notebook on Project Tab.
         self.ui.notebook.setCurrentWidget(self.ui.project_tab)
 
-        self.set_ui_title(name="New Project")
-
+        self.set_ui_title(name=_("New Project - Not saved"))
 
     def obj_properties(self):
         self.report_usage("obj_properties()")
@@ -6328,7 +6370,7 @@ class App(QtCore.QObject):
 
         try:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
+                                                                   directory=self.get_last_folder(), filter=_filter_)
         except TypeError:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
 
@@ -6357,7 +6399,7 @@ class App(QtCore.QObject):
 
         try:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
+                                                                   directory=self.get_last_folder(), filter=_filter_)
         except TypeError:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
 
@@ -6387,7 +6429,7 @@ class App(QtCore.QObject):
                    "All Files (*.*)"
         try:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
+                                                                   directory=self.get_last_folder(), filter=_filter_)
         except TypeError:
             filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
 
@@ -6413,9 +6455,9 @@ class App(QtCore.QObject):
         _filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
+                                                                 directory=self.get_last_folder(), filter=_filter_)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter = _filter_)
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter=_filter_)
 
         # The Qt methods above will return a QString which can cause problems later.
         # So far json.dump() will fail to serialize it.
@@ -6443,10 +6485,10 @@ class App(QtCore.QObject):
         _filter_ = "FlatCAM Config (*.FlatConfig);;FlatCAM Config (*.json);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
-                                                         directory=self.data_path, filter=_filter_)
+                                                                 directory=self.data_path, filter=_filter_)
         except TypeError:
             filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
-                                                                 filter = _filter_)
+                                                                 filter=_filter_)
 
         if filename == "":
             self.inform.emit(_("[WARNING_NOTCL] Open Config cancelled."))
@@ -6488,14 +6530,14 @@ class App(QtCore.QObject):
 
         name = obj.options["name"]
 
-        filter = "SVG File (*.svg);;All Files (*.*)"
+        _filter = "SVG File (*.svg);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
                 caption=_("Export SVG"),
                 directory=self.get_last_save_folder() + '/' + str(name),
-                filter=filter)
+                filter=_filter)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG"), filter=filter)
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG"), filter=_filter)
 
         filename = str(filename)
 
@@ -6563,14 +6605,14 @@ class App(QtCore.QObject):
 
         name = self.collection.get_active().options["name"]
 
-        filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
+        _filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
                 caption="Save Gerber source file",
                 directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
+                filter=_filter)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Gerber source file"), filter=filter)
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Gerber source file"), filter=_filter)
 
         filename = str(filename)
 
@@ -6604,14 +6646,14 @@ class App(QtCore.QObject):
 
         name = self.collection.get_active().options["name"]
 
-        filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
+        _filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
                 caption=_("Save Excellon source file"),
                 directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
+                filter=_filter)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Excellon source file"), filter=filter)
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Excellon source file"), filter=_filter)
 
         filename = str(filename)
 
@@ -6645,14 +6687,14 @@ class App(QtCore.QObject):
 
         name = self.collection.get_active().options["name"]
 
-        filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
+        _filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
                 caption=_("Export Excellon"),
                 directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
+                filter=_filter)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Excellon"), filter=filter)
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Excellon"), filter=_filter)
 
         filename = str(filename)
 
@@ -6823,9 +6865,9 @@ class App(QtCore.QObject):
                     self.worker_task.emit({'fcn': self.import_dxf,
                                            'params': [filename, type_of_obj]})
 
-    # ################################################################################################################ ##
-    # # ## The following section has the functions that are displayed and call the Editor tab CNCJob Tab ############### ##
-    # ################################################################################################################ ##
+    # ###############################################################################################################
+    # ### The following section has the functions that are displayed and call the Editor tab CNCJob Tab #############
+    # ###############################################################################################################
 
     def init_code_editor(self, name):
         # Signals section
@@ -6979,7 +7021,7 @@ class App(QtCore.QObject):
             _filter_ = "TCL script (*.FlatScript);;TCL script (*.TCL);;TCL script (*.TXT);;All Files (*.*)"
             try:
                 filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"),
-                                                             directory=self.get_last_folder(), filter=_filter_)
+                                                                     directory=self.get_last_folder(), filter=_filter_)
             except TypeError:
                 filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"), filter=_filter_)
 
@@ -7448,7 +7490,7 @@ class App(QtCore.QObject):
         ewhole = self.defaults["excellon_exp_integer"]
         efract = self.defaults["excellon_exp_decimals"]
         ezeros = self.defaults["excellon_exp_zeros"]
-        eformat = self.defaults[ "excellon_exp_format"]
+        eformat = self.defaults["excellon_exp_format"]
 
         fc_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         if fc_units == 'MM':
@@ -8594,22 +8636,27 @@ class App(QtCore.QObject):
             _('<b>Shortcut Key List</b>'))
         sel_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
         sel_title.setFrameStyle(QtWidgets.QFrame.NoFrame)
-        # font = self.sel_title.font()
-        # font.setPointSize(12)
-        # self.sel_title.setFont(font)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("notebook_font_size"):
+            fsize = settings.value('notebook_font_size', type=int)
+        else:
+            fsize = 12
+
+        tsize = fsize + int(fsize / 2)
 
         selected_text = _('''
-<p><span style="font-size:14px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span></p>
+<p><span style="font-size:{tsize}px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span></p>
 
-<p><span style="font-size:10px"><strong>Details</strong>:<br />
+<p><span style="font-size:{fsize}px"><strong>Details</strong>:<br />
 The normal flow when working in FlatCAM is the following:</span></p>
 
 <ol>
-	<li><span style="font-size:10px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into FlatCAM using either the menu&#39;s, toolbars, key shortcuts or even dragging and dropping the files on the GUI.<br />
+	<li><span style="font-size:{fsize}px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into FlatCAM using either the menu&#39;s, toolbars, key shortcuts or even dragging and dropping the files on the GUI.<br />
 	<br />
 	You can also load a <strong>FlatCAM project</strong> by double clicking on the project file, drag &amp; drop of the file into the FLATCAM GUI or through the menu/toolbar links offered within the app.</span><br />
 	&nbsp;</li>
-	<li><span style="font-size:10px">Once an object is available in the Project Tab, by selecting it and then focusing on <strong>SELECTED TAB </strong>(more simpler is to double click the object name in the Project Tab), <strong>SELECTED TAB </strong>will be updated with the object properties according to it&#39;s kind: Gerber, Excellon, Geometry or CNCJob object.<br />
+	<li><span style="font-size:{fsize}px">Once an object is available in the Project Tab, by selecting it and then focusing on <strong>SELECTED TAB </strong>(more simpler is to double click the object name in the Project Tab), <strong>SELECTED TAB </strong>will be updated with the object properties according to it&#39;s kind: Gerber, Excellon, Geometry or CNCJob object.<br />
 	<br />
 	If the selection of the object is done on the canvas by single click instead, and the <strong>SELECTED TAB</strong> is in focus, again the object properties will be displayed into the Selected Tab. Alternatively, double clicking on the object on the canvas will bring the <strong>SELECTED TAB</strong> and populate it even if it was out of focus.<br />
 	<br />
@@ -8618,44 +8665,15 @@ The normal flow when working in FlatCAM is the following:</span></p>
 	<strong>Gerber/Excellon Object</strong> -&gt; Change Param -&gt; Generate Geometry -&gt;<strong> Geometry Object </strong>-&gt; Add tools (change param in Selected Tab) -&gt; Generate CNCJob -&gt;<strong> CNCJob Object </strong>-&gt; Verify GCode (through Edit CNC Code) and/or append/prepend to GCode (again, done in <strong>SELECTED TAB)&nbsp;</strong>-&gt; Save GCode</span></li>
 </ol>
 
-<p><span style="font-size:10px">A list of key shortcuts is available through an menu entry in <strong>Help -&gt; Shortcuts List</strong>&nbsp;or through it&#39;s own key shortcut: <strng>F3</strong>.</span></p>
+<p><span style="font-size:{fsize}px">A list of key shortcuts is available through an menu entry in <strong>Help -&gt; Shortcuts List</strong>&nbsp;or through it&#39;s own key shortcut: <strng>F3</strong>.</span></p>
 
-        ''')
+        '''.format(fsize=fsize, tsize=tsize))
 
         sel_title.setText(selected_text)
         sel_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
 
         self.ui.selected_scroll_area.setWidget(sel_title)
 
-#         tool_title = QtWidgets.QTextEdit(
-#             '<b>Shortcut Key List</b>')
-#         tool_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
-#         tool_title.setFrameStyle(QtWidgets.QFrame.NoFrame)
-#         # font = self.sel_title.font()
-#         # font.setPointSize(12)
-#         # self.sel_title.setFont(font)
-#
-#         tool_text = '''
-# <p><span style="font-size:14px"><strong>Tool Tab - Choose an Item in Tools Menu</strong></span></p>
-#
-# <p><span style="font-size:10px"><strong>Details</strong>:<br />
-# Some of the functionality of FlatCAM have been implemented as tools (a sort of plugins). </span></p>
-#
-# <p><span style="font-size:10px">Most of the tools are accessible through&nbsp;the Tools menu or by using the associated shortcut keys.<br />
-# Each such a tool, if it needs an object to be used as a source it will provide the way to select this object(s) through a series of comboboxes. The result of using a tool is either a Geometry, an information that can be used in the app or it can be a file that can be saved.</span></p>
-#
-# <ol>
-# </ol>
-#
-# <p><span style="font-size:10px">A list of key shortcuts is available through an menu entry in <strong>Help -&gt; Shortcuts List</strong>&nbsp;or through it&#39;s own key shortcut: &#39;`&#39; (key left to 1).</span></p>
-#
-#                 '''
-#
-#         tool_title.setText(tool_text)
-#         tool_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
-#
-#         self.ui.tool_scroll_area.setWidget(tool_title)
-
     def setup_obj_classes(self):
         """
         Sets up application specifics on the FlatCAMObj class.
@@ -8937,7 +8955,7 @@ The normal flow when working in FlatCAM is the following:</span></p>
         to_quit = quit
         self.save_timer = QtCore.QTimer()
         self.save_timer.setInterval(delay)
-        self.save_timer.timeout.connect(lambda : self.check_project_file_size(filename=filename, quit=to_quit))
+        self.save_timer.timeout.connect(lambda: self.check_project_file_size(filename=filename, quit=to_quit))
         self.save_timer.start()
 
     def check_project_file_size(self, filename, quit=None):

+ 42 - 12
FlatCAMObj.py

@@ -358,7 +358,7 @@ class FlatCAMObj(QtCore.QObject):
                 pass
 
         if threaded is False:
-            worker_task(self)
+            worker_task(app_obj=self)
         else:
             self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
 
@@ -879,23 +879,48 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             if invert:
                 try:
-                    if type(geom) is MultiPolygon:
+                    try:
                         pl = []
                         for p in geom:
                             if p is not None:
-                                pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                                if isinstance(p, Polygon):
+                                    pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                                elif isinstance(p, LinearRing):
+                                    pl.append(Polygon(p.coords[::-1]))
                         geom = MultiPolygon(pl)
-                    elif type(geom) is Polygon and geom is not None:
-                        geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
-                    else:
-                        log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry")
+                    except TypeError:
+                        if isinstance(geom, Polygon) and geom is not None:
+                            geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+                        elif isinstance(geom, LinearRing) and geom is not None:
+                            geom = Polygon(geom.coords[::-1])
+                        else:
+                            log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
+                                      type(geom))
                 except Exception as e:
                     log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
                     return 'fail'
             return geom
 
-        if float(self.options["isotooldia"]) < 0:
-            self.options["isotooldia"] = -self.options["isotooldia"]
+            # if invert:
+            #     try:
+            #         if type(geom) is MultiPolygon:
+            #             pl = []
+            #             for p in geom:
+            #                 if p is not None:
+            #                     pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+            #             geom = MultiPolygon(pl)
+            #         elif type(geom) is Polygon and geom is not None:
+            #             geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+            #         else:
+            #             log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
+            #                       type(geom))
+            #     except Exception as e:
+            #         log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
+            #         return 'fail'
+            # return geom
+
+        # if float(self.options["isotooldia"]) < 0:
+        #     self.options["isotooldia"] = -self.options["isotooldia"]
 
         if combine:
             if self.iso_type == 0:
@@ -983,7 +1008,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
                 if empty_cnt == len(geo_obj.solid_geometry):
                     raise ValidationError("Empty Geometry", None)
-                geo_obj.multigeo = True
+
+                # even if combine is checked, one pass is still singlegeo
+                if passes > 1:
+                    geo_obj.multigeo = True
+                else:
+                    geo_obj.multigeo = False
 
             # TODO: Do something if this is None. Offer changing name?
             self.app.new_object("geometry", iso_name, iso_init)
@@ -5937,9 +5967,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             self.annotation.clear(update=True)
 
         if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
+            self.annotation.enabled = True
         else:
-            self.app.plotcanvas.text_collection.enabled = False
+            self.annotation.enabled = False
 
     def on_annotation_change(self):
         if self.ui.annotation_cb.get_value():

+ 9 - 3
ObjectCollection.py

@@ -15,7 +15,7 @@ from FlatCAMObj import *
 import inspect  # TODO: Remove
 import FlatCAMApp
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSettings
 # import webbrowser
 
 import gettext
@@ -256,8 +256,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # self.view.setAcceptDrops(True)
         # self.view.setDropIndicatorShown(True)
 
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("notebook_font_size"):
+            fsize = settings.value('notebook_font_size', type=int)
+        else:
+            fsize = 12
+
         font = QtGui.QFont()
-        font.setPixelSize(12)
+        font.setPixelSize(fsize)
         font.setFamily("Seagoe UI")
         self.view.setFont(font)
 
@@ -312,7 +318,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             for obj in self.get_selected():
                 if type(obj) != FlatCAMGeometry:
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
-                if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon:
+                if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMGerber:
                     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
                     self.app.ui.menuprojectviewsource.setVisible(False)

+ 41 - 0
README.md

@@ -9,6 +9,47 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+14.08.2019
+
+- fixed the loading of Excellon with slots and the saving of edited Excellon object in regard of slots, in Excellon Editor
+- fixed the Delete tool, Select tool in Excellon Editor to work for Slots too
+
+13.08.2019
+
+- added new option in ToolSub: the ability to close (or not) the resulting paths when using tool on Geometry objects. Added also a new category in the Edit -> Preferences -> Tools, the Substractor Tool Options
+- some PEP8 changes in FlatCAMApp.py
+- added new settings in Edit -> Preferences -> General for Notebook Font size (set font size for the items in Project Tree and for text in Selected Tab) and for canvas Axis font size. The values are stored in QSettings.
+- updated translations
+- fixed a bug in FCDoubleSpinner GUI element
+- added a new parameter in NCC tool named offset. If the offset is used then the copper clearing will finish to a set distance of the copper features
+- fixed bugs in Geometry Editor
+- added protection's against the 'bowtie' geometries for Subtract Tool in Geometry Editor
+- added all the tools from Geometry Editor to the the contextual menu
+- fixed bug in Add Text Tool in Geometry Editor that gave error when clicking to place text without having text in the box
+- added all the tools from Gerber Editor to the the contextual menu
+- added the menu entry "Edit" in the Project contextual menu for Gerber objects
+- started to work in adding slots and slots array in Excellon Editor
+- in FCSlot finished the utility geometry and the GUI for it
+
+12.08.2019
+
+- done regression to solve the bug with multiple passes cutting from the copper features (I should remember not to make mods here)
+- if 'combine' is checked in Gerber isolation but there is only one pass, the resulting geometry will still be single geo
+- the 'passes' entry was changed to a IntSpinner so it will allow passes to be entered only in range (1, 999) - it will not allow entry of 0 which may create some issues
+- improved the FlatCAMGerber.isolate() function to work for geometry in the form of list and also in case that the elements of the list are LinearRings (like when doing the Exterior Isolation)
+- in NCC Tool made sure that at each run the old objects are deleted
+- fixed bug in camlib.Gerber.parse_lines() Gerber parser where for Allegro Gerber files the Gerber units were incorrectly detected
+- improved Mark Area Tool in Gerber Editor such that at each launch the previous markings are deleted
+
+11.08.2019
+
+- small changes regarding the Project Title
+- trying to fix reported bugs
+- made sure that the annotations are deleted when the object that contain them is deleted
+- fixed issue where the annotations for all the CNCJob objects are toggled together whenever the ones for an single object are toggled
+- optimizations in GeoEditor
+- updated translations
+
 10.08.2019
 
 - added new feature in NCC Tool: now another object can be used as reference for the area extent to be cleared of copper

+ 11 - 15
camlib.py

@@ -554,22 +554,18 @@ class Geometry(object):
             if follow:
                 geo_iso = self.follow_geometry
             else:
+                if isinstance(self.solid_geometry, list):
+                    temp_geo = cascaded_union(self.solid_geometry)
+                else:
+                    temp_geo = self.solid_geometry
+
+                # Remember: do not make a buffer for each element in the solid_geometry because it will cut into
+                # other copper features
                 if corner is None:
-                    try:
-                        __ = iter(self.solid_geometry)
-                        for el in self.solid_geometry:
-                            geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-                    except TypeError:
-                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
+                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
                 else:
-                    try:
-                        __ = iter(self.solid_geometry)
-                        for el in self.solid_geometry:
-                            geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                                     join_style=corner))
-                    except TypeError:
-                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                                             join_style=corner)
+                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
+                                              join_style=corner)
 
         # end of replaced block
         if follow:
@@ -2362,7 +2358,7 @@ class Gerber (Geometry):
                         "D-no zero suppression)" % self.gerber_zeros)
                     log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
 
-                    self.gerber_units = match.group(1)
+                    self.gerber_units = match.group(5)
                     log.debug("Gerber units found = %s" % self.gerber_units)
                     # Changed for issue #80
                     self.convert_units(match.group(5))

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 761 - 53
flatcamEditors/FlatCAMExcEditor.py


+ 91 - 63
flatcamEditors/FlatCAMGeoEditor.py

@@ -2633,15 +2633,20 @@ class FCText(FCShapeTool):
         # Create new geometry
         dx = point[0]
         dy = point[1]
-        try:
-            self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
-        except Exception as e:
-            log.debug("Font geometry is empty or incorrect: %s" % str(e))
-            self.draw_app.app.inform.emit(_("[ERROR]Font not supported. Only Regular, Bold, Italic and BoldItalic are "
-                                          "supported. Error: %s") % str(e))
-            self.text_gui.text_path = []
-            self.text_gui.hide_tool()
-            self.draw_app.select_tool('select')
+
+        if self.text_gui.text_path:
+            try:
+                self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
+            except Exception as e:
+                log.debug("Font geometry is empty or incorrect: %s" % str(e))
+                self.draw_app.app.inform.emit(_("[ERROR]Font not supported. Only Regular, Bold, Italic and BoldItalic are "
+                                              "supported. Error: %s") % str(e))
+                self.text_gui.text_path = []
+                self.text_gui.hide_tool()
+                self.draw_app.select_tool('select')
+                return
+        else:
+            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] No text to add."))
             return
 
         self.text_gui.text_path = []
@@ -2912,7 +2917,7 @@ class FCTransform(FCShapeTool):
         self.draw_app = draw_app
         self.app = draw_app.app
 
-        self.draw_app.app.infrom.emit(_("Shape transformations ..."))
+        self.draw_app.app.inform.emit(_("Shape transformations ..."))
         self.origin = (0, 0)
         self.draw_app.transform_tool.run()
 
@@ -3277,7 +3282,22 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Geometry Editor
         self.app.ui.draw_line.triggered.connect(self.draw_tool_path)
         self.app.ui.draw_rect.triggered.connect(self.draw_tool_rectangle)
+
+        self.app.ui.draw_circle.triggered.connect(lambda: self.select_tool('circle'))
+        self.app.ui.draw_poly.triggered.connect(lambda: self.select_tool('polygon'))
+        self.app.ui.draw_arc.triggered.connect(lambda: self.select_tool('arc'))
+
+        self.app.ui.draw_text.triggered.connect(lambda: self.select_tool('text'))
+        self.app.ui.draw_buffer.triggered.connect(lambda: self.select_tool('buffer'))
+        self.app.ui.draw_paint.triggered.connect(lambda: self.select_tool('paint'))
+        self.app.ui.draw_eraser.triggered.connect(lambda: self.select_tool('eraser'))
+
+        self.app.ui.draw_union.triggered.connect(self.union)
+        self.app.ui.draw_intersect.triggered.connect(self.intersection)
+        self.app.ui.draw_substract.triggered.connect(self.subtract)
         self.app.ui.draw_cut.triggered.connect(self.cutpath)
+        self.app.ui.draw_transform.triggered.connect(lambda: self.select_tool('transform'))
+
         self.app.ui.draw_move.triggered.connect(self.on_move)
 
     def disconnect_canvas_event_handlers(self):
@@ -3332,6 +3352,32 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except (TypeError, AttributeError):
             pass
 
+        self.app.ui.draw_circle.triggered.disconnect()
+        self.app.ui.draw_poly.triggered.disconnect()
+        self.app.ui.draw_arc.triggered.disconnect()
+
+        self.app.ui.draw_text.triggered.disconnect()
+        self.app.ui.draw_buffer.triggered.disconnect()
+        self.app.ui.draw_paint.triggered.disconnect()
+        self.app.ui.draw_eraser.triggered.disconnect()
+
+        try:
+            self.app.ui.draw_union.triggered.disconnect(self.union)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_intersect.triggered.disconnect(self.intersection)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_substract.triggered.disconnect(self.subtract)
+        except (TypeError, AttributeError):
+            pass
+
+        self.app.ui.draw_transform.triggered.disconnect()
+
     def add_shape(self, shape):
         """
         Adds a shape to the shape storage.
@@ -3497,7 +3543,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -3507,7 +3553,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.pos = (self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = False
 
-        if event.button is 1:
+        if event.button == 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
@@ -3536,20 +3582,17 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
                     self.replot()
-
             else:
                 self.app.log.debug("No active tool to respond to click!")
 
     def on_canvas_move(self, event):
         """
         Called on 'mouse_move' event
-
         event.pos have canvas screen coordinates
 
         :param event: Event object dispatched by VisPy SceneCavas
         :return: None
         """
-
         pos = self.canvas.vispy_canvas.translate_coords(event.pos)
         event.xdata, event.ydata = pos[0], pos[1]
 
@@ -3559,8 +3602,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.popMenu.mouse_is_panning = False
 
         # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == 2 and event.is_dragging == 1:
-            self.app.ui.popMenu.mouse_is_panning = True
+        if event.button == 2:
+            if event.is_dragging:
+                self.app.ui.popMenu.mouse_is_panning = True
+                # return
+            else:
+                self.app.ui.popMenu.mouse_is_panning = False
+
+        if self.active_tool is None:
             return
 
         try:
@@ -3569,11 +3618,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except TypeError:
             return
 
-        if self.active_tool is None:
-            return
-
-        # # ## Snap coordinates
-        if self.app.grid_status():
+        # ### Snap coordinates ###
+        if self.app.grid_status() == True:
             x, y = self.snap(x, y)
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -3597,19 +3643,19 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                            "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
 
-        if event.button == 1 and event.is_dragging == 1 and isinstance(self.active_tool, FCEraser):
+        if event.button == 1 and event.is_dragging and isinstance(self.active_tool, FCEraser):
             pass
         else:
-            # # ## Utility geometry (animated)
+            # ### Utility geometry (animated) ###
             geo = self.active_tool.utility_geometry(data=(x, y))
             if isinstance(geo, DrawToolShape) and geo.geo is not None:
                 # Remove any previous utility shape
                 self.tool_shape.clear(update=True)
                 self.draw_utility_geometry(geo=geo)
 
-        # # ## Selection area on canvas section # ##
+        # ### Selection area on canvas section ###
         dx = pos[0] - self.pos[0]
-        if event.is_dragging == 1 and event.button == 1:
+        if event.is_dragging and event.button == 1:
             self.app.delete_selection_shape()
             if dx < 0:
                 self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
@@ -3625,7 +3671,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
     def on_geo_click_release(self, event):
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             pos = self.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = (pos_canvas[0], pos_canvas[1])
@@ -3633,8 +3679,20 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
         # canvas menu
         try:
-            if event.button == 2:  # right click
-                if self.app.ui.popMenu.mouse_is_panning is False:
+            # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+            # selection and then select a type of selection ("enclosing" or "touching")
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+                elif isinstance(self.active_tool, FCSelect):
+                    # Dispatch event to active_tool
+                    # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+                    # self.app.inform.emit(msg)
+                    self.replot()
+            elif event.button == 2:  # right click
+                if self.app.ui.popMenu.mouse_is_panning == False:
                     if self.in_action is False:
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -3661,38 +3719,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                 self.on_shape_complete()
                                 self.app.inform.emit(_("[success] Done."))
                                 self.select_tool(self.active_tool.name)
-
-                                # MS: always return to the Select Tool if modifier key is not pressed
-                                # else return to the current tool
-                                # key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                                # if self.app.defaults["global_mselect_key"] == 'Control':
-                                #     modifier_to_use = Qt.ControlModifier
-                                # else:
-                                #     modifier_to_use = Qt.ShiftModifier
-                                #
-                                # if key_modifier == modifier_to_use:
-                                #     self.select_tool(self.active_tool.name)
-                                # else:
-                                #     self.select_tool("select")
-
-        except Exception as e:
-            log.warning("Error: %s" % str(e))
-            return
-
-        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
-        # selection and then select a type of selection ("enclosing" or "touching")
-        try:
-            if event.button == 1:  # left click
-                if self.app.selection_type is not None:
-                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
-                    self.app.selection_type = None
-                elif isinstance(self.active_tool, FCSelect):
-                    # Dispatch event to active_tool
-                    # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
-                    self.active_tool.click_release((self.pos[0], self.pos[1]))
-                    # self.app.inform.emit(msg)
-                    self.replot()
-
         except Exception as e:
             log.warning("Error: %s" % str(e))
             return
@@ -4107,8 +4133,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
         selected = self.get_selected()
         try:
             tools = selected[1:]
-            toolgeo = unary_union([shp.geo for shp in tools])
-            result = selected[0].geo.difference(toolgeo)
+            toolgeo = unary_union([shp.geo for shp in tools]).buffer(0.0000001)
+            target = selected[0].geo
+            target = target.buffer(0.0000001)
+            result = target.difference(toolgeo)
 
             for_deletion = [s for s in self.get_selected()]
             for shape in for_deletion:

+ 50 - 4
flatcamEditors/FlatCAMGrbEditor.py

@@ -530,7 +530,7 @@ class FCPadArray(FCShapeTool):
                         )
                     if 'follow' in geo_el:
                         new_geo_el['follow'] = affinity.translate(
-                            geo_el['solid'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
+                            geo_el['follow'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
                         )
                     geo_el_list.append(new_geo_el)
 
@@ -1775,6 +1775,9 @@ class FCMarkArea(FCShapeTool):
         self.draw_app.hide_tool('all')
         self.draw_app.ma_tool_frame.show()
 
+        # clear previous marking
+        self.draw_app.ma_annotation.clear(update=True)
+
         try:
             self.draw_app.ma_threshold__button.clicked.disconnect()
         except (TypeError, AttributeError):
@@ -3471,6 +3474,16 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.ui.grb_draw_track.triggered.connect(self.on_track_add)
         self.app.ui.grb_draw_region.triggered.connect(self.on_region_add)
 
+        self.app.ui.grb_draw_poligonize.triggered.connect(self.on_poligonize)
+        self.app.ui.grb_draw_semidisc.triggered.connect(self.on_add_semidisc)
+        self.app.ui.grb_draw_disc.triggered.connect(self.on_disc_add)
+        self.app.ui.grb_draw_buffer.triggered.connect(lambda: self.select_tool("buffer"))
+        self.app.ui.grb_draw_scale.triggered.connect(lambda: self.select_tool("scale"))
+        self.app.ui.grb_draw_markarea.triggered.connect(lambda: self.select_tool("markarea"))
+        self.app.ui.grb_draw_eraser.triggered.connect(self.on_eraser)
+        self.app.ui.grb_draw_transformations.triggered.connect(self.on_transform)
+
+
     def disconnect_canvas_event_handlers(self):
 
         # we restore the key and mouse control to FlatCAMApp method
@@ -3527,6 +3540,39 @@ class FlatCAMGrbEditor(QtCore.QObject):
         except (TypeError, AttributeError):
             pass
 
+        try:
+            self.app.ui.grb_draw_poligonize.triggered.disconnect(self.on_poligonize)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_semidisc.triggered.diconnect(self.on_add_semidisc)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_disc.triggered.disconnect(self.on_disc_add)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_buffer.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_scale.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_markarea.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_eraser.triggered.disconnect(self.on_eraser)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_transformations.triggered.disconnect(self.on_transform)
+        except (TypeError, AttributeError):
+            pass
+
     def clear(self):
         self.active_tool = None
         self.selected = []
@@ -3986,7 +4032,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -4048,7 +4094,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = (pos_canvas[0], pos_canvas[1])
@@ -4201,7 +4247,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
 
         # # ## Snap coordinates
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             x, y = self.app.geo_editor.snap(x, y)
             self.app.app_cursor.enabled = True
             # Update cursor

+ 153 - 4
flatcamGUI/FlatCAMGUI.py

@@ -449,6 +449,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                                      _('Add Drill\tD'))
         self.exc_editor_menu.addSeparator()
 
+        self.exc_add_array_slot_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon('share/rectangle32.png'), _('Add Slot Array\tQ'))
+        self.exc_add_slot_menuitem = self.exc_editor_menu.addAction(QtGui.QIcon('share/plus16.png'),
+                                                                     _('Add Slot\tW'))
+        self.exc_editor_menu.addSeparator()
+
         self.exc_resize_drill_menuitem = self.exc_editor_menu.addAction(
             QtGui.QIcon('share/resize16.png'), _('Resize Drill(S)\tR')
         )
@@ -654,6 +660,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Drill Hole'))
         self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
             QtGui.QIcon('share/addarray16.png'), _('Add Drill Hole Array'))
+        self.add_slot_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Slot'))
+        self.add_slot_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon('share/addarray16.png'), _('Add Slot Array'))
         self.resize_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/resize16.png'), _('Resize Drill'))
         self.exc_edit_toolbar.addSeparator()
 
@@ -1437,6 +1446,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>M</strong></td>
                         <td>&nbsp;Move Drill(s)</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20" width="89"><strong>Q</strong></td>
+                        <td width="194">&nbsp;Add Slot Array</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>R</strong></td>
                         <td>&nbsp;Resize Drill(s)</td>
@@ -1445,6 +1458,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>T</strong></td>
                         <td>&nbsp;Add a new Tool</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20" width="89"><strong>W</strong></td>
+                        <td width="194">&nbsp;Add Slot</td>
+                    </tr>
                     <tr height="20">
                         <td height="20">&nbsp;</td>
                         <td>&nbsp;</td>
@@ -1609,21 +1626,58 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.popMenu.addSeparator()
 
         self.g_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/draw32.png'), _("Geo Editor"))
-        self.draw_line = self.g_editor_cmenu.addAction(QtGui.QIcon('share/path32.png'), _("Line"))
+        self.draw_line = self.g_editor_cmenu.addAction(QtGui.QIcon('share/path32.png'), _("Path"))
         self.draw_rect = self.g_editor_cmenu.addAction(QtGui.QIcon('share/rectangle32.png'), _("Rectangle"))
+        self.g_editor_cmenu.addSeparator()
+        self.draw_circle = self.g_editor_cmenu.addAction(QtGui.QIcon('share/circle32.png'), _("Circle"))
+        self.draw_poly = self.g_editor_cmenu.addAction(QtGui.QIcon('share/polygon32.png'), _("Polygon"))
+        self.draw_arc = self.g_editor_cmenu.addAction(QtGui.QIcon('share/arc32.png'), _("Arc"))
+        self.g_editor_cmenu.addSeparator()
+
+        self.draw_text = self.g_editor_cmenu.addAction(QtGui.QIcon('share/text32.png'), _("Text"))
+        self.draw_buffer = self.g_editor_cmenu.addAction(QtGui.QIcon('share/buffer16-2.png'), _("Buffer"))
+        self.draw_paint = self.g_editor_cmenu.addAction(QtGui.QIcon('share/paint20_1.png'), _("Paint"))
+        self.draw_eraser = self.g_editor_cmenu.addAction(QtGui.QIcon('share/eraser26.png'), _("Eraser"))
+        self.g_editor_cmenu.addSeparator()
+
+        self.draw_union = self.g_editor_cmenu.addAction(QtGui.QIcon('share/union32.png'), _("Union"))
+        self.draw_intersect = self.g_editor_cmenu.addAction(QtGui.QIcon('share/intersection32.png'), _("Intersection"))
+        self.draw_substract = self.g_editor_cmenu.addAction(QtGui.QIcon('share/subtract32.png'), _("Substraction"))
         self.draw_cut = self.g_editor_cmenu.addAction(QtGui.QIcon('share/cutpath32.png'), _("Cut"))
+        self.draw_transform = self.g_editor_cmenu.addAction(QtGui.QIcon('share/transform.png'), _("Transformations"))
+
         self.g_editor_cmenu.addSeparator()
         self.draw_move = self.g_editor_cmenu.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
 
         self.grb_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/draw32.png'), _("Gerber Editor"))
         self.grb_draw_pad = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/aperture32.png'), _("Pad"))
         self.grb_draw_pad_array = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/padarray32.png'), _("Pad Array"))
+        self.grb_editor_cmenu.addSeparator()
+
         self.grb_draw_track = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/track32.png'), _("Track"))
         self.grb_draw_region = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/polygon32.png'), _("Region"))
+        self.grb_draw_poligonize = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/poligonize32.png'), _("Poligonize"))
+        self.grb_draw_semidisc = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/semidisc32.png'), _("SemiDisc"))
+        self.grb_draw_disc = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/disc32.png'), _("Disc"))
+        self.grb_editor_cmenu.addSeparator()
+
+        self.grb_draw_buffer = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/buffer16-2.png'), _("Buffer"))
+        self.grb_draw_scale = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/scale32.png'), _("Scale"))
+        self.grb_draw_markarea = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/markarea32.png'), _("Mark Area"))
+        self.grb_draw_eraser = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/eraser26.png'), _("Eraser"))
+        self.grb_editor_cmenu.addSeparator()
+
+        self.grb_draw_transformations = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/transform.png'),
+                                                                        _("Transformations"))
 
         self.e_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/drill32.png'), _("Exc Editor"))
         self.drill = self.e_editor_cmenu.addAction(QtGui.QIcon('share/drill32.png'), _("Add Drill"))
         self.drill_array = self.e_editor_cmenu.addAction(QtGui.QIcon('share/addarray32.png'), _("Add Drill Array"))
+        self.e_editor_cmenu.addSeparator()
+        self.slot = self.e_editor_cmenu.addAction(QtGui.QIcon('share/drill32.png'), _("Add Slot"))
+        self.slot_array = self.e_editor_cmenu.addAction(QtGui.QIcon('share/addarray32.png'), _("Add Slot Array"))
+        self.e_editor_cmenu.addSeparator()
+        self.drill_resize= self.e_editor_cmenu.addAction(QtGui.QIcon('share/resize16.png'), _("Resize Drill"))
 
         self.popMenu.addSeparator()
         self.popmenu_copy = self.popMenu.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
@@ -1901,6 +1955,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
             QtGui.QIcon('share/addarray16.png'), _('Add Drill Hole Array'))
         self.resize_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/resize16.png'), _('Resize Drill'))
+        self.add_slot_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Slot'))
+        self.add_slot_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon('share/addarray16.png'), _('Add Slot Array'))
         self.exc_edit_toolbar.addSeparator()
 
         self.copy_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), _('Copy Drill'))
@@ -3280,6 +3337,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         self.tools_solderpaste_group = ToolsSolderpastePrefGroupUI()
         self.tools_solderpaste_group.setMinimumWidth(200)
 
+        self.tools_sub_group = ToolsSubPrefGroupUI()
+        self.tools_sub_group.setMinimumWidth(200)
+
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.tools_ncc_group)
         self.vlay.addWidget(self.tools_paint_group)
@@ -3296,6 +3356,7 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
 
         self.vlay3 = QtWidgets.QVBoxLayout()
         self.vlay3.addWidget(self.tools_solderpaste_group)
+        self.vlay3.addWidget(self.tools_sub_group)
 
         self.layout.addLayout(self.vlay)
         self.layout.addLayout(self.vlay1)
@@ -3713,6 +3774,38 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         )
         self.selection_cb = FCCheckBox()
 
+        self.notebook_font_size_label = QtWidgets.QLabel(_('NB Font Size:'))
+        self.notebook_font_size_label.setToolTip(
+            _("This sets the font size for the elements found in the Notebook.\n"
+              "The notebook is the collapsible area in the left side of the GUI,\n"
+              "and include the Project, Selected and Tool tabs.")
+        )
+
+        self.notebook_font_size_spinner = FCSpinner()
+        self.notebook_font_size_spinner.setRange(8, 40)
+        self.notebook_font_size_spinner.setWrapping(True)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("notebook_font_size"):
+            self.notebook_font_size_spinner.set_value(settings.value('notebook_font_size', type=int))
+        else:
+            self.notebook_font_size_spinner.set_value(12)
+
+        self.axis_font_size_label = QtWidgets.QLabel(_('Axis Font Size:'))
+        self.axis_font_size_label.setToolTip(
+            _("This sets the font size for canvas axis.")
+        )
+
+        self.axis_font_size_spinner = FCSpinner()
+        self.axis_font_size_spinner.setRange(8, 40)
+        self.axis_font_size_spinner.setWrapping(True)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("axis_font_size"):
+            self.axis_font_size_spinner.set_value(settings.value('axis_font_size', type=int))
+        else:
+            self.axis_font_size_spinner.set_value(8)
+
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
 
@@ -3725,6 +3818,10 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         self.form_box.addRow(self.clear_label, self.clear_btn)
         self.form_box.addRow(self.hover_label, self.hover_cb)
         self.form_box.addRow(self.selection_label, self.selection_cb)
+        self.form_box.addRow(QtWidgets.QLabel(''))
+        self.form_box.addRow(self.notebook_font_size_label, self.notebook_font_size_spinner)
+        self.form_box.addRow(self.axis_font_size_label, self.axis_font_size_spinner)
+
 
         # Add the QFormLayout that holds the Application general defaults
         # to the main layout of this TAB
@@ -4092,7 +4189,8 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
               "number (integer) of tool widths.")
         )
         grid0.addWidget(passlabel, 1, 0)
-        self.iso_width_entry = IntEntry()
+        self.iso_width_entry = FCSpinner()
+        self.iso_width_entry.setRange(1, 999)
         grid0.addWidget(self.iso_width_entry, 1, 1)
 
         # Pass overlap
@@ -5875,6 +5973,35 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         self.ncc_rest_cb = FCCheckBox()
         grid0.addWidget(self.ncc_rest_cb, 6, 1)
 
+        # ## NCC Offset choice
+        self.ncc_offset_choice_label = QtWidgets.QLabel(_("Offset:"))
+        self.ncc_offset_choice_label.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.\n"
+              "The value can be between 0 and 10 FlatCAM units.")
+        )
+        grid0.addWidget(self.ncc_offset_choice_label, 7, 0)
+        self.ncc_choice_offset_cb = FCCheckBox()
+        grid0.addWidget(self.ncc_choice_offset_cb, 7, 1)
+
+        # ## NCC Offset value
+        self.ncc_offset_label = QtWidgets.QLabel(_("Offset value:"))
+        self.ncc_offset_label.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.\n"
+              "The value can be between 0 and 10 FlatCAM units.")
+        )
+        grid0.addWidget(self.ncc_offset_label, 8, 0)
+        self.ncc_offset_spinner = FCDoubleSpinner()
+        self.ncc_offset_spinner.set_range(0.00, 10.00)
+        self.ncc_offset_spinner.set_precision(4)
+        self.ncc_offset_spinner.setWrapping(True)
+        self.ncc_offset_spinner.setSingleStep(0.1)
+
+        grid0.addWidget(self.ncc_offset_spinner, 8, 1)
+
         # ## Reference
         self.reference_radio = RadioSet([{'label': _('Itself'), 'value': 'itself'},
                                          {'label': _('Box'), 'value': 'box'}])
@@ -5885,8 +6012,8 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
               "Choosing the 'Box' option will do non copper clearing within the box\n"
               "specified by another object different than the one that is copper cleared.")
         )
-        grid0.addWidget(reference_label, 7, 0)
-        grid0.addWidget(self.reference_radio, 7, 1)
+        grid0.addWidget(reference_label, 9, 0)
+        grid0.addWidget(self.reference_radio, 9, 1)
 
         self.layout.addStretch()
 
@@ -6707,6 +6834,28 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
         self.layout.addStretch()
 
 
+class ToolsSubPrefGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+
+        super(ToolsSubPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str(_("Substractor Tool Options")))
+
+        # ## Solder Paste Dispensing
+        self.sublabel = QtWidgets.QLabel(_("<b>Parameters:</b>"))
+        self.sublabel.setToolTip(
+            _("A tool to substract one Gerber or Geometry object\n"
+              "from another of the same type.")
+        )
+        self.layout.addWidget(self.sublabel)
+
+        self.close_paths_cb = FCCheckBox(_("Close paths"))
+        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry substractor object."))
+        self.layout.addWidget(self.close_paths_cb)
+
+        self.layout.addStretch()
+
+
 class FlatCAMActivityView(QtWidgets.QWidget):
 
     def __init__(self, parent=None):

+ 1 - 1
flatcamGUI/GUIElements.py

@@ -1609,7 +1609,7 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
 
     def set_value(self, val):
         try:
-            k = int(val)
+            k = float(val)
         except Exception as e:
             log.debug(str(e))
             return

+ 2 - 1
flatcamGUI/ObjectUI.py

@@ -276,7 +276,8 @@ class GerberObjectUI(ObjectUI):
         )
         passlabel.setMinimumWidth(90)
         grid1.addWidget(passlabel, 1, 0)
-        self.iso_width_entry = IntEntry()
+        self.iso_width_entry = FCSpinner()
+        self.iso_width_entry.setRange(1, 999)
         grid1.addWidget(self.iso_width_entry, 1, 1)
 
         overlabel = QtWidgets.QLabel(_('Pass overlap:'))

+ 13 - 5
flatcamGUI/VisPyCanvas.py

@@ -8,12 +8,13 @@
 
 import numpy as np
 from PyQt5.QtGui import QPalette
+from PyQt5.QtCore import QSettings
 import vispy.scene as scene
 from vispy.scene.cameras.base_camera import BaseCamera
 from vispy.color import Color
 import time
 
-white = Color("#ffffff" )
+white = Color("#ffffff")
 black = Color("#000000")
 
 
@@ -35,12 +36,19 @@ class VisPyCanvas(scene.SceneCanvas):
         top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
 
-        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("axis_font_size"):
+            a_fsize = settings.value('axis_font_size', type=int)
+        else:
+            a_fsize = 8
+
+        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=a_fsize)
         self.yaxis.width_max = 55
         self.grid_widget.add_widget(self.yaxis, row=1, col=0)
 
-        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
-        self.xaxis.height_max = 25
+        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=a_fsize,
+                                      anchors=['center', 'bottom'])
+        self.xaxis.height_max = 30
         self.grid_widget.add_widget(self.xaxis, row=2, col=1)
 
         right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
@@ -48,7 +56,7 @@ class VisPyCanvas(scene.SceneCanvas):
         right_padding.width_max = 0
 
         view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
-        view.camera = Camera(aspect=1, rect=(-25,-25,150,150))
+        view.camera = Camera(aspect=1, rect=(-25, -25, 150, 150))
 
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
         # it is necessary to call manually

+ 2 - 2
flatcamTools/ToolMeasurement.py

@@ -249,7 +249,7 @@ class Measurement(FlatCAMTool):
         if event.button == 1:
             pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
             # if GRID is active we need to get the snapped positions
-            if self.app.grid_status():
+            if self.app.grid_status() == True:
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             else:
                 pos = pos_canvas[0], pos_canvas[1]
@@ -287,7 +287,7 @@ class Measurement(FlatCAMTool):
     def on_mouse_move_meas(self, event):
         try:  # May fail in case mouse not within axes
             pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-            if self.app.grid_status():
+            if self.app.grid_status() == True:
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
                 self.app.app_cursor.enabled = True
                 # Update cursor

+ 90 - 9
flatcamTools/ToolNonCopperClear.py

@@ -235,6 +235,43 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_rest_cb = FCCheckBox()
         grid3.addWidget(self.ncc_rest_cb, 6, 1)
 
+        # ## NCC Offset choice
+        self.ncc_offset_choice_label = QtWidgets.QLabel(_("Offset:"))
+        self.ncc_offset_choice_label.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.\n"
+              "The value can be between 0 and 10 FlatCAM units.")
+        )
+        grid3.addWidget(self.ncc_offset_choice_label, 7, 0)
+        self.ncc_choice_offset_cb = FCCheckBox()
+        grid3.addWidget(self.ncc_choice_offset_cb, 7, 1)
+
+        # ## NCC Offset value
+        self.ncc_offset_label = QtWidgets.QLabel(_("Offset value:"))
+        self.ncc_offset_label.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.\n"
+              "The value can be between 0 and 10 FlatCAM units.")
+        )
+        grid3.addWidget(self.ncc_offset_label, 8, 0)
+        self.ncc_offset_spinner = FCDoubleSpinner()
+        self.ncc_offset_spinner.set_range(0.00, 10.00)
+        self.ncc_offset_spinner.set_precision(4)
+        self.ncc_offset_spinner.setWrapping(True)
+
+        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        if units == 'MM':
+            self.ncc_offset_spinner.setSingleStep(0.1)
+        else:
+            self.ncc_offset_spinner.setSingleStep(0.01)
+
+        grid3.addWidget(self.ncc_offset_spinner, 8, 1)
+
+        self.ncc_offset_label.hide()
+        self.ncc_offset_spinner.hide()
+
         # ## Reference
         self.reference_radio = RadioSet([{'label': _('Itself'), 'value': 'itself'},
                                          {'label': _('Box'), 'value': 'box'}])
@@ -245,8 +282,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
               "- 'Box': will do non copper clearing within the box\n"
               "specified by the object selected in the Ref. Object combobox.")
         )
-        grid3.addWidget(self.reference_label, 7, 0)
-        grid3.addWidget(self.reference_radio, 7, 1)
+        grid3.addWidget(self.reference_label, 9, 0)
+        grid3.addWidget(self.reference_radio, 9, 1)
 
         grid4 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid4)
@@ -308,6 +345,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.reference_radio.group_toggle_fn = self.on_toggle_reference
+        self.ncc_choice_offset_cb.stateChanged.connect(self.on_offset_choice)
 
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
@@ -332,6 +370,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
         FlatCAMTool.run(self)
         self.set_tool_ui()
 
+        # reset those objects on a new run
+        self.ncc_obj = None
+        self.bound_obj = None
+        self.obj_name = ''
+        self.bound_obj_name = ''
+
         self.build_ui()
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
 
@@ -537,6 +581,14 @@ class NonCopperClear(FlatCAMTool, Gerber):
             self.box_combo_type.show()
             self.box_combo_type_label.show()
 
+    def on_offset_choice(self, state):
+        if state:
+            self.ncc_offset_label.show()
+            self.ncc_offset_spinner.show()
+        else:
+            self.ncc_offset_label.hide()
+            self.ncc_offset_spinner.hide()
+
     def on_tool_add(self, dia=None, muted=None):
 
         self.ui_disconnect()
@@ -692,6 +744,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.build_ui()
 
     def on_ncc(self):
+        self.bound_obj = None
+        self.ncc_obj = None
 
         try:
             over = float(self.ncc_overlap_entry.get_value())
@@ -722,6 +776,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 return
         margin = margin if margin is not None else float(self.app.defaults["tools_nccmargin"])
 
+        try:
+            ncc_offset_value = float(self.ncc_offset_spinner.get_value())
+        except ValueError:
+            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                   "use a number."))
+            return
+        ncc_offset_value = ncc_offset_value if ncc_offset_value is not None \
+            else float(self.app.defaults["tools_ncc_offset_value"])
+
         connect = self.ncc_connect_cb.get_value()
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
 
@@ -773,7 +836,14 @@ class NonCopperClear(FlatCAMTool, Gerber):
             return
 
         # calculate the empty area by subtracting the solid_geometry from the object bounding box geometry
-        empty = self.ncc_obj.get_empty_area(bounding_box)
+        if self.ncc_choice_offset_cb.isChecked():
+            self.app.inform.emit(_("[WARNING_NOTCL] Buffering ..."))
+            offseted_geo = self.ncc_obj.solid_geometry.buffer(distance=ncc_offset_value)
+            self.app.inform.emit(_("[success] Buffering finished ..."))
+            empty = self.get_ncc_empty_area(target=offseted_geo, boundary=bounding_box)
+        else:
+            empty = self.get_ncc_empty_area(target=self.ncc_obj.solid_geometry, boundary=bounding_box)
+
         if type(empty) is Polygon:
             empty = MultiPolygon([empty])
 
@@ -1049,24 +1119,24 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 app_obj.new_object("geometry", name, initialize_rm)
             except Exception as e:
                 proc.done()
-                self.app.inform.emit(_('[ERROR_NOTCL] NCCTool.clear_non_copper_rest() --> %s') % str(e))
+                app_obj.inform.emit(_('[ERROR_NOTCL] NCCTool.clear_non_copper_rest() --> %s') % str(e))
                 return
 
             if app_obj.poly_not_cleared is True:
-                self.app.inform.emit('[success] NCC Tool finished.')
+                app_obj.inform.emit('[success] NCC Tool finished.')
                 # focus on Selected Tab
-                self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+                app_obj.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
             else:
-                self.app.inform.emit(_('[ERROR_NOTCL] NCC Tool finished but could not clear the object '
+                app_obj.inform.emit(_('[ERROR_NOTCL] NCC Tool finished but could not clear the object '
                                      'with current settings.'))
                 # focus on Project Tab
-                self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+                app_obj.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
             proc.done()
             # reset the variable for next use
             app_obj.poly_not_cleared = False
 
             self.tools_frame.hide()
-            self.app.ui.notebook.setTabText(2, "Tools")
+            app_obj.ui.notebook.setTabText(2, "Tools")
 
         # Promise object with the new name
         self.app.collection.promise(name)
@@ -1074,5 +1144,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # Background
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
+    @staticmethod
+    def get_ncc_empty_area(target, boundary=None):
+        """
+        Returns the complement of target geometry within
+        the given boundary polygon. If not specified, it defaults to
+        the rectangular bounding box of target geometry.
+        """
+        if boundary is None:
+            boundary = target.envelope
+        return boundary.difference(target)
+
     def reset_fields(self):
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 4 - 4
flatcamTools/ToolPaint.py

@@ -807,7 +807,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                     self.app.plotcanvas.vis_disconnect('mouse_press', doit)
 
                     pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-                    if self.app.grid_status():
+                    if self.app.grid_status() == True:
                         pos = self.app.geo_editor.snap(pos[0], pos[1])
 
                     self.paint_poly(self.paint_obj,
@@ -836,7 +836,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         self.app.inform.emit(_("[WARNING_NOTCL] Click the end point of the paint area."))
 
                         self.cursor_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-                        if self.app.grid_status():
+                        if self.app.grid_status() == True:
                             self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1])
                     else:
                         self.app.inform.emit(_("Done."))
@@ -844,7 +844,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         self.app.delete_selection_shape()
 
                         curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-                        if self.app.grid_status():
+                        if self.app.grid_status() == True:
                             curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
 
                         x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
@@ -874,7 +874,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                 curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
                 self.app.app_cursor.enabled = False
 
-                if self.app.grid_status():
+                if self.app.grid_status() == True:
                     self.app.app_cursor.enabled = True
                     # Update cursor
                     curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])

+ 64 - 8
flatcamTools/ToolSub.py

@@ -130,6 +130,10 @@ class ToolSub(FlatCAMTool):
 
         form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
 
+        self.close_paths_cb = FCCheckBox(_("Close paths"))
+        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry substractor object."))
+        self.tools_box.addWidget(self.close_paths_cb)
+
         self.intersect_geo_btn = FCButton(_('Substract Geometry'))
         self.intersect_geo_btn.setToolTip(
             _("Will remove the area occupied by the substractor\n"
@@ -217,6 +221,7 @@ class ToolSub(FlatCAMTool):
 
     def set_tool_ui(self):
         self.tools_frame.show()
+        self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
 
     def on_grb_intersection_click(self):
         # reset previous values
@@ -231,7 +236,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
 
-        # Get source object.
+        # Get target object.
         try:
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
         except Exception as e:
@@ -244,7 +249,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
 
-        # Get source object.
+        # Get substractor object.
         try:
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
         except Exception as e:
@@ -424,7 +429,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
 
-        # Get source object.
+        # Get target object.
         try:
             self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
         except Exception as e:
@@ -437,7 +442,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
 
-        # Get source object.
+        # Get substractor object.
         try:
             self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
         except Exception as e:
@@ -496,10 +501,58 @@ class ToolSub(FlatCAMTool):
             text = _("Parsing tool %s geometry ...") % str(tool)
 
         with self.app.proc_container.new(text):
-            new_geo = (cascaded_union(geo)).difference(self.sub_union)
-            if new_geo:
-                if not new_geo.is_empty:
-                    new_geometry.append(new_geo)
+            # resulting paths are closed resulting into Polygons
+            if self.close_paths_cb.isChecked():
+                new_geo = (cascaded_union(geo)).difference(self.sub_union)
+                if new_geo:
+                    if not new_geo.is_empty:
+                        new_geometry.append(new_geo)
+            # resulting paths are unclosed resulting in a multitude of rings
+            else:
+                try:
+                    for geo_elem in geo:
+                        if isinstance(geo_elem, Polygon):
+                            for ring in self.poly2rings(geo_elem):
+                                new_geo = ring.difference(self.sub_union)
+                                if new_geo:
+                                    if 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(self.sub_union)
+                                    if new_geo:
+                                        if not new_geo.is_empty:
+                                            new_geometry.append(new_geo)
+                        elif isinstance(geo_elem, LineString):
+                            new_geo = geo_elem.difference(self.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(self.sub_union)
+                                if new_geo:
+                                    if not new_geo.is_empty:
+                                        new_geometry.append(new_geo)
+                except TypeError:
+                    if isinstance(geo, Polygon):
+                        for ring in self.poly2rings(geo):
+                            new_geo = ring.difference(self.sub_union)
+                            if new_geo:
+                                if not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
+                    elif isinstance(geo, LineString):
+                        new_geo = geo.difference(self.sub_union)
+                        if new_geo:
+                            if not new_geo.is_empty:
+                                new_geometry.append(new_geo)
+                    elif isinstance(geo, MultiLineString):
+                        for line_elem in geo:
+                            new_geo = line_elem.difference(self.sub_union)
+                            if new_geo:
+                                if not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
 
         if new_geometry:
             if tool == "single":
@@ -620,4 +673,7 @@ class ToolSub(FlatCAMTool):
         self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
 
+    @staticmethod
+    def poly2rings(poly):
+        return [poly.exterior] + [interior for interior in poly.interiors]
 # end of file

BIN
locale/de/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 211 - 203
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 212 - 203
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 211 - 203
locale/es/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 211 - 203
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 211 - 203
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 210 - 202
locale/ru/LC_MESSAGES/strings.po


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 237 - 229
locale_template/strings.pot


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio