Explorar o código

jpcgt/flatcam/Beta слито с Beta

Camellan %!s(int64=5) %!d(string=hai) anos
pai
achega
d9b48a101f

+ 24 - 0
CHANGELOG.md

@@ -7,6 +7,30 @@ CHANGELOG for FlatCAM beta
 
 
 =================================================
 =================================================
 
 
+7.05.2020
+
+- added a fix so the app close is now clean, with exit code 0 as set
+- added the ability to add exclusion areas from the Excellon object too. Now there is a different in color to differentiate from which type of object the exclusion areas were added but they all serve the same purpose
+
+6.05.2020
+
+- wip in adding Exclusion areas in Geometry object; each Geometry object has now a storage for shapes (exclusion shapes, should I make them more general?)
+- changed the above: too many shapes collections and the performance will go down. Created a class ExclusionAreas that holds all the require properties and the Object UI elements will connect to it's methods. This way I can apply this feature to Excellon object too (who is a special type of Geometry Object)
+- handled the New project event and the object deletion (when all objects are deleted then the exclusion areas will be deleted too)
+- solved issue with new parameter end_xy when it is None
+- solved issue with applying theme and not making the change in the Preferences UI. In Preferences UI the theme radio is always Light (white)
+- now the annotations will invert the selected color in the Preferences, when selecting Dark theme 
+
+5.05.2020
+
+- fixed an issue that made the preprocessors combo boxes in Preferences not to load and display the saved value fro the file
+- some PEP8 corrections
+
+4.05.2020
+
+- in detachable tabs, Linux loose the reference of the detached tab and on close of the detachable tabs will gave a 'segmentation fault' error. Solved it by not deleting the reference in case of Unix-like systems
+- some strings added to translation strings
+
 3.05.2020
 3.05.2020
 
 
 - small changes to allow making the x86 installer that is made from a Python 3.5 run FlatCAM beta 
 - small changes to allow making the x86 installer that is made from a Python 3.5 run FlatCAM beta 

+ 70 - 47
FlatCAMApp.py

@@ -43,7 +43,7 @@ import socket
 # ####################################################################################################################
 # ####################################################################################################################
 
 
 # Diverse
 # Diverse
-from FlatCAMCommon import LoudDict, color_variant
+from FlatCAMCommon import LoudDict, color_variant, ExclusionAreas
 from FlatCAMBookmark import BookmarkManager
 from FlatCAMBookmark import BookmarkManager
 from FlatCAMDB import ToolsDB2
 from FlatCAMDB import ToolsDB2
 
 
@@ -522,6 +522,9 @@ class App(QtCore.QObject):
             else:
             else:
                 self.cursor_color_3D = 'gray'
                 self.cursor_color_3D = 'gray'
 
 
+        # update the defaults dict with the setting in QSetting
+        self.defaults['global_theme'] = theme
+
         self.ui.geom_update[int, int, int, int, int].connect(self.save_geometry)
         self.ui.geom_update[int, int, int, int, int].connect(self.save_geometry)
         self.ui.final_save.connect(self.final_save)
         self.ui.final_save.connect(self.final_save)
 
 
@@ -543,6 +546,47 @@ class App(QtCore.QObject):
         self.save_project_auto_update()
         self.save_project_auto_update()
         self.autosave_timer.timeout.connect(self.save_project_auto)
         self.autosave_timer.timeout.connect(self.save_project_auto)
 
 
+        # ###########################################################################################################
+        # #################################### LOAD PREPROCESSORS ###################################################
+        # ###########################################################################################################
+
+        # ----------------------------------------- WARNING --------------------------------------------------------
+        # Preprocessors need to be loaded before the Preferences Manager builds the Preferences
+        # That's because the number of preprocessors can vary and here the comboboxes are populated
+        # -----------------------------------------------------------------------------------------------------------
+
+        # a dictionary that have as keys the name of the preprocessor files and the value is the class from
+        # the preprocessor file
+        self.preprocessors = load_preprocessors(self)
+
+        # make sure that always the 'default' preprocessor is the first item in the dictionary
+        if 'default' in self.preprocessors.keys():
+            new_ppp_dict = {}
+
+            # add the 'default' name first in the dict after removing from the preprocessor's dictionary
+            default_pp = self.preprocessors.pop('default')
+            new_ppp_dict['default'] = default_pp
+
+            # then add the rest of the keys
+            for name, val_class in self.preprocessors.items():
+                new_ppp_dict[name] = val_class
+
+            # and now put back the ordered dict with 'default' key first
+            self.preprocessors = new_ppp_dict
+
+        for name in list(self.preprocessors.keys()):
+            # 'Paste' preprocessors are to be used only in the Solder Paste Dispensing Tool
+            if name.partition('_')[0] == 'Paste':
+                self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
+                continue
+
+            self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
+            # HPGL preprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
+            if name == 'hpgl':
+                continue
+
+            self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
+
         # ###########################################################################################################
         # ###########################################################################################################
         # ##################################### UPDATE PREFERENCES GUI FORMS ########################################
         # ##################################### UPDATE PREFERENCES GUI FORMS ########################################
         # ###########################################################################################################
         # ###########################################################################################################
@@ -558,9 +602,7 @@ class App(QtCore.QObject):
         # ##################################### FIRST RUN SECTION ###################################################
         # ##################################### FIRST RUN SECTION ###################################################
         # ################################ It's done only once after install   #####################################
         # ################################ It's done only once after install   #####################################
         # ###########################################################################################################
         # ###########################################################################################################
-
         if self.defaults["first_run"] is True:
         if self.defaults["first_run"] is True:
-
             # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT'
             # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT'
             initial_lay = 'minimal'
             initial_lay = 'minimal'
             self.ui.general_defaults_form.general_gui_group.on_layout(lay=initial_lay)
             self.ui.general_defaults_form.general_gui_group.on_layout(lay=initial_lay)
@@ -585,42 +627,6 @@ class App(QtCore.QObject):
         self.project_filename = None
         self.project_filename = None
         self.toggle_units_ignore = False
         self.toggle_units_ignore = False
 
 
-        # ###########################################################################################################
-        # #################################### LOAD PREPROCESSORS ###################################################
-        # ###########################################################################################################
-
-        # a dictionary that have as keys the name of the preprocessor files and the value is the class from
-        # the preprocessor file
-        self.preprocessors = load_preprocessors(self)
-
-        # make sure that always the 'default' preprocessor is the first item in the dictionary
-        if 'default' in self.preprocessors.keys():
-            new_ppp_dict = {}
-
-            # add the 'default' name first in the dict after removing from the preprocessor's dictionary
-            default_pp = self.preprocessors.pop('default')
-            new_ppp_dict['default'] = default_pp
-
-            # then add the rest of the keys
-            for name, val_class in self.preprocessors.items():
-                new_ppp_dict[name] = val_class
-
-            # and now put back the ordered dict with 'default' key first
-            self.preprocessors = new_ppp_dict
-
-        for name in list(self.preprocessors.keys()):
-            # 'Paste' preprocessors are to be used only in the Solder Paste Dispensing Tool
-            if name.partition('_')[0] == 'Paste':
-                self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
-                continue
-
-            self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
-            # HPGL preprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
-            if name == 'hpgl':
-                continue
-
-            self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
-
         # ###########################################################################################################
         # ###########################################################################################################
         # ########################################## LOAD LANGUAGES  ################################################
         # ########################################## LOAD LANGUAGES  ################################################
         # ###########################################################################################################
         # ###########################################################################################################
@@ -1598,6 +1604,11 @@ class App(QtCore.QObject):
             self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio.set_value('T')
             self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio.set_value('T')
             self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio.setDisabled(True)
             self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio.setDisabled(True)
 
 
+        # ###########################################################################################################
+        # ########################################### EXCLUSION AREAS ###############################################
+        # ###########################################################################################################
+        self.exc_areas = ExclusionAreas(app=self)
+
         # ###########################################################################################################
         # ###########################################################################################################
         # ##################################### Finished the CONSTRUCTOR ############################################
         # ##################################### Finished the CONSTRUCTOR ############################################
         # ###########################################################################################################
         # ###########################################################################################################
@@ -2577,7 +2588,7 @@ class App(QtCore.QObject):
         self.date = self.date.replace(' ', '_')
         self.date = self.date.replace(' ', '_')
 
 
         filter__ = "HTML File .html (*.html);;TXT File .txt (*.txt);;All Files (*.*)"
         filter__ = "HTML File .html (*.html);;TXT File .txt (*.txt);;All Files (*.*)"
-        path_to_save = self.defaults["global_last_save_folder"] if\
+        path_to_save = self.defaults["global_last_save_folder"] if \
             self.defaults["global_last_save_folder"] is not None else self.data_path
             self.defaults["global_last_save_folder"] is not None else self.data_path
         try:
         try:
             filename, _f = FCFileSaveDialog.get_saved_filename(
             filename, _f = FCFileSaveDialog.get_saved_filename(
@@ -3251,7 +3262,7 @@ class App(QtCore.QObject):
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("E-mail")), 0, 2)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("E-mail")), 0, 2)
 
 
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Juan Pablo Caram"), 1, 0)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Juan Pablo Caram"), 1, 0)
-                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Program Author"), 1, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % _("Program Author")), 1, 1)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<>"), 1, 2)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<>"), 1, 2)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Denis Hayrullin"), 2, 0)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Denis Hayrullin"), 2, 0)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Kamil Sopko"), 3, 0)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Kamil Sopko"), 3, 0)
@@ -3347,7 +3358,7 @@ class App(QtCore.QObject):
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Italian"), 4, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Italian"), 4, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Golfetto Massimiliano"), 4, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Golfetto Massimiliano"), 4, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 4, 2)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 4, 2)
-                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "pcb@golfetto.eu"), 4, 3)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<golfetto.pcb@gmail.com>"), 4, 3)
 
 
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 5, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 5, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Tr)"), 5, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Tr)"), 5, 1)
@@ -3587,14 +3598,13 @@ class App(QtCore.QObject):
 
 
         # try to quit the Socket opened by ArgsThread class
         # try to quit the Socket opened by ArgsThread class
         try:
         try:
-            self.new_launch.thread_exit = True
-            self.new_launch.listener.close()
+            self.new_launch.stop.emit()
         except Exception as err:
         except Exception as err:
             log.debug("App.quit_application() --> %s" % str(err))
             log.debug("App.quit_application() --> %s" % str(err))
 
 
         # try to quit the QThread that run ArgsThread class
         # try to quit the QThread that run ArgsThread class
         try:
         try:
-            self.th.terminate()
+            self.th.quit()
         except Exception as e:
         except Exception as e:
             log.debug("App.quit_application() --> %s" % str(e))
             log.debug("App.quit_application() --> %s" % str(e))
 
 
@@ -3615,7 +3625,6 @@ class App(QtCore.QObject):
 
 
     @staticmethod
     @staticmethod
     def kill_app():
     def kill_app():
-        # QtCore.QCoreApplication.quit()
         QtWidgets.qApp.quit()
         QtWidgets.qApp.quit()
         # When the main event loop is not started yet in which case the qApp.quit() will do nothing
         # When the main event loop is not started yet in which case the qApp.quit() will do nothing
         # we use the following command
         # we use the following command
@@ -5115,7 +5124,7 @@ class App(QtCore.QObject):
 
 
                     for obj_active in self.collection.get_selected():
                     for obj_active in self.collection.get_selected():
                         # if the deleted object is GerberObject then make sure to delete the possible mark shapes
                         # if the deleted object is GerberObject then make sure to delete the possible mark shapes
-                        if isinstance(obj_active, GerberObject):
+                        if obj_active.kind == 'gerber':
                             for el in obj_active.mark_shapes:
                             for el in obj_active.mark_shapes:
                                 obj_active.mark_shapes[el].clear(update=True)
                                 obj_active.mark_shapes[el].clear(update=True)
                                 obj_active.mark_shapes[el].enabled = False
                                 obj_active.mark_shapes[el].enabled = False
@@ -5138,6 +5147,10 @@ class App(QtCore.QObject):
                     self.inform.emit('%s...' % _("Object(s) deleted"))
                     self.inform.emit('%s...' % _("Object(s) deleted"))
                     # make sure that the selection shape is deleted, too
                     # make sure that the selection shape is deleted, too
                     self.delete_selection_shape()
                     self.delete_selection_shape()
+
+                    # if there are no longer objects delete also the exclusion areas shapes
+                    if not self.collection.get_list():
+                        self.exc_areas.clear_shapes()
                 else:
                 else:
                     self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
                     self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
         else:
         else:
@@ -7405,6 +7418,9 @@ class App(QtCore.QObject):
                 except AttributeError:
                 except AttributeError:
                     pass
                     pass
 
 
+        # delete the exclusion areas
+        self.exc_areas.clear_shapes()
+
         # tcl needs to be reinitialized, otherwise old shell variables etc  remains
         # tcl needs to be reinitialized, otherwise old shell variables etc  remains
         self.shell.init_tcl()
         self.shell.init_tcl()
 
 
@@ -10869,6 +10885,7 @@ class App(QtCore.QObject):
 class ArgsThread(QtCore.QObject):
 class ArgsThread(QtCore.QObject):
     open_signal = pyqtSignal(list)
     open_signal = pyqtSignal(list)
     start = pyqtSignal()
     start = pyqtSignal()
+    stop = pyqtSignal()
 
 
     if sys.platform == 'win32':
     if sys.platform == 'win32':
         address = (r'\\.\pipe\NPtest', 'AF_PIPE')
         address = (r'\\.\pipe\NPtest', 'AF_PIPE')
@@ -10881,6 +10898,7 @@ class ArgsThread(QtCore.QObject):
         self.thread_exit = False
         self.thread_exit = False
 
 
         self.start.connect(self.run)
         self.start.connect(self.run)
+        self.stop.connect(self.close_listener)
 
 
     def my_loop(self, address):
     def my_loop(self, address):
         try:
         try:
@@ -10924,4 +10942,9 @@ class ArgsThread(QtCore.QObject):
     def run(self):
     def run(self):
         self.my_loop(self.address)
         self.my_loop(self.address)
 
 
+    @pyqtSlot()
+    def close_listener(self):
+        self.thread_exit = True
+        self.listener.close()
+
 # end of file
 # end of file

+ 395 - 0
FlatCAMCommon.py

@@ -11,6 +11,14 @@
 # Date: 11/4/2019                                          #
 # Date: 11/4/2019                                          #
 # ##########################################################
 # ##########################################################
 
 
+from shapely.geometry import Polygon, MultiPolygon
+
+from flatcamGUI.VisPyVisuals import ShapeCollection
+from FlatCAMTool import FlatCAMTool
+
+import numpy as np
+import re
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -119,3 +127,390 @@ def color_variant(hex_color, bright_factor=1):
         new_rgb.append(mod_color_hex)
         new_rgb.append(mod_color_hex)
 
 
     return "#" + "".join([i for i in new_rgb])
     return "#" + "".join([i for i in new_rgb])
+
+
+class ExclusionAreas:
+
+    def __init__(self, app):
+        self.app = app
+
+        # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry
+        # VisPy visuals
+        if self.app.is_legacy is False:
+            try:
+                self.exclusion_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
+            except AttributeError:
+                self.exclusion_shapes = None
+        else:
+            from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            self.exclusion_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="exclusion")
+
+        # Event signals disconnect id holders
+        self.mr = None
+        self.mm = None
+        self.kp = None
+
+        # variables to be used in area exclusion
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+        self.points = []
+        self.poly_drawn = False
+
+        '''
+        Here we store the exclusion shapes and some other information's
+        Each list element is a dictionary with the format:
+        
+        {
+            "obj_type":   string ("excellon" or "geometry")   <- self.obj_type
+            "shape":      Shapely polygon
+            "strategy":   string ("over" or "around")         <- self.strategy
+            "overz":      float                               <- self.over_z
+        }
+        '''
+        self.exclusion_areas_storage = []
+
+        self.mouse_is_dragging = False
+
+        self.solid_geometry = []
+        self.obj_type = None
+
+        self.shape_type = 'square'  # TODO use the self.app.defaults when made general (not in Geo object Pref UI)
+        self.over_z = 0.1
+        self.strategy = None
+        self.cnc_button = None
+
+    def on_add_area_click(self, shape_button, overz_button, strategy_radio, cnc_button, solid_geo, obj_type):
+        """
+
+        :param shape_button:    a FCButton that has the value for the shape
+        :param overz_button:    a FCDoubleSpinner that holds the Over Z value
+        :param strategy_radio:  a RadioSet button with the strategy value
+        :param cnc_button:      a FCButton in Object UI that when clicked the CNCJob is created
+                                We have a reference here so we can change the color signifying that exclusion areas are
+                                available.
+        :param solid_geo:       reference to the object solid geometry for which we add exclusion areas
+        :param obj_type:        Type of FlatCAM object that called this method
+        :type obj_type:         String: "excellon" or "geometry"
+        :return:
+        """
+        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
+        self.app.call_source = 'geometry'
+
+        self.shape_type = shape_button.get_value()
+        self.over_z = overz_button.get_value()
+        self.strategy = strategy_radio.get_value()
+        self.cnc_button = cnc_button
+
+        self.solid_geometry = solid_geo
+        self.obj_type = obj_type
+
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+
+        self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+        self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+        # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
+
+    # To be called after clicking on the plot.
+    def on_mouse_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        event_pos = self.app.plotcanvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+        else:
+            curr_pos = (event_pos[0], event_pos[1])
+
+        x1, y1 = curr_pos[0], curr_pos[1]
+
+        # shape_type = self.ui.area_shape_radio.get_value()
+
+        # do clear area only for left mouse clicks
+        if event.button == 1:
+            if self.shape_type == "square":
+                if self.first_click is False:
+                    self.first_click = True
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
+
+                    self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                    if self.app.grid_status():
+                        self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+                else:
+                    self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                    self.app.delete_selection_shape()
+
+                    x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+
+                    pt1 = (x0, y0)
+                    pt2 = (x1, y0)
+                    pt3 = (x1, y1)
+                    pt4 = (x0, y1)
+
+                    new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+
+                    # {
+                    #     "obj_type":   string("excellon" or "geometry") < - self.obj_type
+                    #     "shape":      Shapely polygon
+                    #     "strategy":   string("over" or "around") < - self.strategy
+                    #     "overz":      float < - self.over_z
+                    # }
+                    new_el = {
+                        "obj_type":     self.obj_type,
+                        "shape":        new_rectangle,
+                        "strategy":     self.strategy,
+                        "overz":        self.over_z
+                    }
+                    self.exclusion_areas_storage.append(new_el)
+
+                    if self.obj_type == 'excellon':
+                        color = "#FF7400"
+                        face_color = "#FF7400BF"
+                    else:
+                        color = "#098a8f"
+                        face_color = "#098a8fBF"
+
+                    # add a temporary shape on canvas
+                    FlatCAMTool.draw_tool_selection_shape(
+                        self, old_coords=(x0, y0), coords=(x1, y1),
+                        color=color,
+                        face_color=face_color,
+                        shapes_storage=self.exclusion_shapes)
+
+                    self.first_click = False
+                    return
+            else:
+                self.points.append((x1, y1))
+
+                if len(self.points) > 1:
+                    self.poly_drawn = True
+                    self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
+
+                return ""
+        elif event.button == right_button and self.mouse_is_dragging is False:
+
+            shape_type = self.shape_type
+
+            if shape_type == "square":
+                self.first_click = False
+            else:
+                # if we finish to add a polygon
+                if self.poly_drawn is True:
+                    try:
+                        # try to add the point where we last clicked if it is not already in the self.points
+                        last_pt = (x1, y1)
+                        if last_pt != self.points[-1]:
+                            self.points.append(last_pt)
+                    except IndexError:
+                        pass
+
+                    # we need to add a Polygon and a Polygon can be made only from at least 3 points
+                    if len(self.points) > 2:
+                        FlatCAMTool.delete_moving_selection_shape(self)
+                        pol = Polygon(self.points)
+                        # do not add invalid polygons even if they are drawn by utility geometry
+                        if pol.is_valid:
+                            # {
+                            #     "obj_type":   string("excellon" or "geometry") < - self.obj_type
+                            #     "shape":      Shapely polygon
+                            #     "strategy":   string("over" or "around") < - self.strategy
+                            #     "overz":      float < - self.over_z
+                            # }
+                            new_el = {
+                                "obj_type": self.obj_type,
+                                "shape": pol,
+                                "strategy": self.strategy,
+                                "overz": self.over_z
+                            }
+                            self.exclusion_areas_storage.append(new_el)
+
+                            if self.obj_type == 'excellon':
+                                color = "#FF7400"
+                                face_color = "#FF7400BF"
+                            else:
+                                color = "#098a8f"
+                                face_color = "#098a8fBF"
+
+                            FlatCAMTool.draw_selection_shape_polygon(
+                                self, points=self.points,
+                                color=color,
+                                face_color=face_color,
+                                shapes_storage=self.exclusion_shapes)
+                            self.app.inform.emit(
+                                _("Zone added. Click to start adding next zone or right click to finish."))
+
+                    self.points = []
+                    self.poly_drawn = False
+                    return
+
+            # FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
+                # self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            self.app.call_source = 'app'
+
+            if len(self.exclusion_areas_storage) == 0:
+                return
+
+            self.app.inform.emit(
+                "[success] %s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
+
+            for el in self.exclusion_areas_storage:
+                if el["shape"].intersects(MultiPolygon(self.solid_geometry)):
+                    self.on_clear_area_click()
+                    self.app.inform.emit(
+                        "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
+                    return
+
+            self.app.inform.emit(
+                "[success] %s" % _("Exclusion areas added."))
+            self.cnc_button.setStyleSheet("""
+                                    QPushButton
+                                    {
+                                        font-weight: bold;
+                                        color: orange;
+                                    }
+                                    """)
+            self.cnc_button.setToolTip(
+                '%s %s' % (_("Generate the CNC Job object."), _("With Exclusion areas."))
+            )
+
+            for k in self.exclusion_areas_storage:
+                print(k)
+
+    def area_disconnect(self):
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.mr)
+            self.app.plotcanvas.graph_event_disconnect(self.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                              self.app.on_mouse_click_over_plot)
+        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                              self.app.on_mouse_move_over_plot)
+        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                              self.app.on_mouse_click_release_over_plot)
+        self.points = []
+        self.poly_drawn = False
+        self.exclusion_areas_storage = []
+
+        FlatCAMTool.delete_moving_selection_shape(self)
+        # FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+        self.app.call_source = "app"
+        self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
+
+    # called on mouse move
+    def on_mouse_move(self, event):
+        shape_type = self.shape_type
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
+
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        # detect mouse dragging motion
+        if event_is_dragging is True:
+            self.mouse_is_dragging = True
+        else:
+            self.mouse_is_dragging = False
+
+        # update the cursor position
+        if self.app.grid_status():
+            # Update cursor
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+
+            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
+
+        # update the positions on status bar
+        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
+        if self.cursor_pos is None:
+            self.cursor_pos = (0, 0)
+
+        self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
+        self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        if self.obj_type == 'excellon':
+            color = "#FF7400"
+            face_color = "#FF7400BF"
+        else:
+            color = "#098a8f"
+            face_color = "#098a8fBF"
+
+        # draw the utility geometry
+        if shape_type == "square":
+            if self.first_click:
+                self.app.delete_selection_shape()
+
+                self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                     color=color,
+                                                     face_color=face_color,
+                                                     coords=(curr_pos[0], curr_pos[1]))
+        else:
+            FlatCAMTool.delete_moving_selection_shape(self)
+            FlatCAMTool.draw_moving_selection_shape_poly(
+                self, points=self.points,
+                color=color,
+                face_color=face_color,
+                data=(curr_pos[0], curr_pos[1]))
+
+    def on_clear_area_click(self):
+        self.clear_shapes()
+
+        # restore the default StyleSheet
+        self.cnc_button.setStyleSheet("")
+        # update the StyleSheet
+        self.cnc_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
+
+    def clear_shapes(self):
+        self.exclusion_areas_storage.clear()
+        FlatCAMTool.delete_moving_selection_shape(self)
+        self.app.delete_selection_shape()
+        FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)

+ 53 - 17
FlatCAMTool.py

@@ -110,6 +110,11 @@ class FlatCAMTool(QtWidgets.QWidget):
         :return:
         :return:
         """
         """
 
 
+        if 'shapes_storage' in kwargs:
+            s_storage = kwargs['shapes_storage']
+        else:
+            s_storage = self.app.tool_shapes
+
         if 'color' in kwargs:
         if 'color' in kwargs:
             color = kwargs['color']
             color = kwargs['color']
         else:
         else:
@@ -139,10 +144,9 @@ class FlatCAMTool(QtWidgets.QWidget):
 
 
         color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
         color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
 
 
-        self.app.tool_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
-                                 layer=0, tolerance=None)
+        s_storage.add(sel_rect, color=color, face_color=color_t, update=True, layer=0, tolerance=None)
         if self.app.is_legacy is True:
         if self.app.is_legacy is True:
-            self.app.tool_shapes.redraw()
+            s_storage.redraw()
 
 
     def draw_selection_shape_polygon(self, points, **kwargs):
     def draw_selection_shape_polygon(self, points, **kwargs):
         """
         """
@@ -151,6 +155,12 @@ class FlatCAMTool(QtWidgets.QWidget):
         :param kwargs:
         :param kwargs:
         :return:
         :return:
         """
         """
+
+        if 'shapes_storage' in kwargs:
+            s_storage = kwargs['shapes_storage']
+        else:
+            s_storage = self.app.tool_shapes
+
         if 'color' in kwargs:
         if 'color' in kwargs:
             color = kwargs['color']
             color = kwargs['color']
         else:
         else:
@@ -165,6 +175,7 @@ class FlatCAMTool(QtWidgets.QWidget):
             face_alpha = kwargs['face_alpha']
             face_alpha = kwargs['face_alpha']
         else:
         else:
             face_alpha = 0.3
             face_alpha = 0.3
+
         if len(points) < 3:
         if len(points) < 3:
             sel_rect = LineString(points)
             sel_rect = LineString(points)
         else:
         else:
@@ -175,14 +186,24 @@ class FlatCAMTool(QtWidgets.QWidget):
 
 
         color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
         color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
 
 
-        self.app.tool_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
-                                 layer=0, tolerance=None)
+        s_storage.add(sel_rect, color=color, face_color=color_t, update=True, layer=0, tolerance=None)
         if self.app.is_legacy is True:
         if self.app.is_legacy is True:
-            self.app.tool_shapes.redraw()
+            s_storage.redraw()
 
 
-    def delete_tool_selection_shape(self):
-        self.app.tool_shapes.clear()
-        self.app.tool_shapes.redraw()
+    def delete_tool_selection_shape(self, **kwargs):
+        """
+
+        :param kwargs:
+        :return:
+        """
+
+        if 'shapes_storage' in kwargs:
+            s_storage = kwargs['shapes_storage']
+        else:
+            s_storage = self.app.tool_shapes
+
+        s_storage.clear()
+        s_storage.redraw()
 
 
     def draw_moving_selection_shape_poly(self, points, data, **kwargs):
     def draw_moving_selection_shape_poly(self, points, data, **kwargs):
         """
         """
@@ -192,6 +213,12 @@ class FlatCAMTool(QtWidgets.QWidget):
         :param kwargs:
         :param kwargs:
         :return:
         :return:
         """
         """
+
+        if 'shapes_storage' in kwargs:
+            s_storage = kwargs['shapes_storage']
+        else:
+            s_storage = self.app.move_tool.sel_shapes
+
         if 'color' in kwargs:
         if 'color' in kwargs:
             color = kwargs['color']
             color = kwargs['color']
         else:
         else:
@@ -226,18 +253,27 @@ class FlatCAMTool(QtWidgets.QWidget):
         color_t_error = "#00000000"
         color_t_error = "#00000000"
 
 
         if geo.is_valid and not geo.is_empty:
         if geo.is_valid and not geo.is_empty:
-            self.app.move_tool.sel_shapes.add(geo, color=color, face_color=color_t, update=True,
-                                              layer=0, tolerance=None)
+            s_storage.add(geo, color=color, face_color=color_t, update=True, layer=0, tolerance=None)
         elif not geo.is_valid:
         elif not geo.is_valid:
-            self.app.move_tool.sel_shapes.add(geo, color="red", face_color=color_t_error, update=True,
-                                              layer=0, tolerance=None)
+            s_storage.add(geo, color="red", face_color=color_t_error, update=True, layer=0, tolerance=None)
 
 
         if self.app.is_legacy is True:
         if self.app.is_legacy is True:
-            self.app.move_tool.sel_shapes.redraw()
+            s_storage.redraw()
+
+    def delete_moving_selection_shape(self, **kwargs):
+        """
+
+        :param kwargs:
+        :return:
+        """
+
+        if 'shapes_storage' in kwargs:
+            s_storage = kwargs['shapes_storage']
+        else:
+            s_storage = self.app.move_tool.sel_shapes
 
 
-    def delete_moving_selection_shape(self):
-        self.app.move_tool.sel_shapes.clear()
-        self.app.move_tool.sel_shapes.redraw()
+        s_storage.clear()
+        s_storage.redraw()
 
 
     def confirmation_message(self, accepted, minval, maxval):
     def confirmation_message(self, accepted, minval, maxval):
         if accepted is False:
         if accepted is False:

+ 1 - 2
FlatCAMTranslation.py

@@ -186,8 +186,7 @@ def restart_program(app, ask=None):
 
 
     # try to quit the Socket opened by ArgsThread class
     # try to quit the Socket opened by ArgsThread class
     try:
     try:
-        app.new_launch.thread_exit = True
-        app.new_launch.listener.close()
+        app.new_launch.stop.emit()
     except Exception as err:
     except Exception as err:
         log.debug("FlatCAMTranslation.restart_program() --> %s" % str(err))
         log.debug("FlatCAMTranslation.restart_program() --> %s" % str(err))
 
 

+ 118 - 157
camlib.py

@@ -249,7 +249,7 @@ class ApertureMacro:
 
 
         pol, dia, x, y = ApertureMacro.default2zero(4, mods)
         pol, dia, x, y = ApertureMacro.default2zero(4, mods)
 
 
-        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
+        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia / 2)}
 
 
     @staticmethod
     @staticmethod
     def make_vectorline(mods):
     def make_vectorline(mods):
@@ -262,7 +262,7 @@ class ApertureMacro:
         pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
         pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
 
 
         line = LineString([(xs, ys), (xe, ye)])
         line = LineString([(xs, ys), (xe, ye)])
-        box = line.buffer(width/2, cap_style=2)
+        box = line.buffer(width / 2, cap_style=2)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
 
         return {"pol": int(pol), "geometry": box_rotated}
         return {"pol": int(pol), "geometry": box_rotated}
@@ -278,7 +278,7 @@ class ApertureMacro:
 
 
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
 
 
-        box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
+        box = shply_box(x - width / 2, y - height / 2, x + width / 2, y + height / 2)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
 
         return {"pol": int(pol), "geometry": box_rotated}
         return {"pol": int(pol), "geometry": box_rotated}
@@ -294,7 +294,7 @@ class ApertureMacro:
 
 
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
 
 
-        box = shply_box(x, y, x+width, y+height)
+        box = shply_box(x, y, x + width, y + height)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
 
         return {"pol": int(pol), "geometry": box_rotated}
         return {"pol": int(pol), "geometry": box_rotated}
@@ -309,12 +309,12 @@ class ApertureMacro:
 
 
         pol = mods[0]
         pol = mods[0]
         n = mods[1]
         n = mods[1]
-        points = [(0, 0)]*(n+1)
+        points = [(0, 0)] * (n + 1)
 
 
-        for i in range(n+1):
-            points[i] = mods[2*i + 2:2*i + 4]
+        for i in range(n + 1):
+            points[i] = mods[2 * i + 2:2 * i + 4]
 
 
-        angle = mods[2*n + 4]
+        angle = mods[2 * n + 4]
 
 
         poly = Polygon(points)
         poly = Polygon(points)
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
@@ -333,11 +333,11 @@ class ApertureMacro:
         """
         """
 
 
         pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
         pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
-        points = [(0, 0)]*nverts
+        points = [(0, 0)] * nverts
 
 
         for i in range(nverts):
         for i in range(nverts):
-            points[i] = (x + 0.5 * dia * np.cos(2*np.pi * i/nverts),
-                         y + 0.5 * dia * np.sin(2*np.pi * i/nverts))
+            points[i] = (x + 0.5 * dia * np.cos(2 * np.pi * i / nverts),
+                         y + 0.5 * dia * np.sin(2 * np.pi * i / nverts))
 
 
         poly = Polygon(points)
         poly = Polygon(points)
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
@@ -358,9 +358,9 @@ class ApertureMacro:
 
 
         x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
         x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
 
 
-        r = dia/2 - thickness/2
-        result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
-        ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)  # Need a copy!
+        r = dia / 2 - thickness / 2
+        result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
+        ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)  # Need a copy!
 
 
         i = 1  # Number of rings created so far
         i = 1  # Number of rings created so far
 
 
@@ -370,13 +370,13 @@ class ApertureMacro:
             r -= thickness + gap
             r -= thickness + gap
             if r <= 0:
             if r <= 0:
                 break
                 break
-            ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+            ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
             result = cascaded_union([result, ring])
             result = cascaded_union([result, ring])
             i += 1
             i += 1
 
 
         # ## Crosshair
         # ## Crosshair
-        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
-        ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
+        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th / 2.0, cap_style=2)
+        ver = LineString([(x, y - cross_len), (x, y + cross_len)]).buffer(cross_th / 2.0, cap_style=2)
         result = cascaded_union([result, hor, ver])
         result = cascaded_union([result, hor, ver])
 
 
         return {"pol": 1, "geometry": result}
         return {"pol": 1, "geometry": result}
@@ -394,9 +394,9 @@ class ApertureMacro:
 
 
         x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
         x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
 
 
-        ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
-        hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
-        vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
+        ring = Point((x, y)).buffer(dout / 2.0).difference(Point((x, y)).buffer(din / 2.0))
+        hline = LineString([(x - dout / 2.0, y), (x + dout / 2.0, y)]).buffer(t / 2.0, cap_style=3)
+        vline = LineString([(x, y - dout / 2.0), (x, y + dout / 2.0)]).buffer(t / 2.0, cap_style=3)
         thermal = ring.difference(hline.union(vline))
         thermal = ring.difference(hline.union(vline))
 
 
         return {"pol": 1, "geometry": thermal}
         return {"pol": 1, "geometry": thermal}
@@ -920,14 +920,16 @@ class Geometry(object):
         Creates contours around geometry at a given
         Creates contours around geometry at a given
         offset distance.
         offset distance.
 
 
-        :param offset: Offset distance.
-        :type offset: float
-        :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
-        :param corner: type of corner for the isolation: 0 = round; 1 = square; 2= beveled (line that connects the ends)
-        :param follow: whether the geometry to be isolated is a follow_geometry
-        :param passes: current pass out of possible multiple passes for which the isolation is done
-        :return: The buffered geometry.
-        :rtype: Shapely.MultiPolygon or Shapely.Polygon
+        :param offset:      Offset distance.
+        :type offset:       float
+        :param geometry     The geometry to work with
+        :param iso_type:    type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
+        :param corner:      type of corner for the isolation:
+                            0 = round; 1 = square; 2= beveled (line that connects the ends)
+        :param follow:      whether the geometry to be isolated is a follow_geometry
+        :param passes:      current pass out of possible multiple passes for which the isolation is done
+        :return:            The buffered geometry.
+        :rtype:             Shapely.MultiPolygon or Shapely.Polygon
         """
         """
 
 
         if self.app.abort_flag:
         if self.app.abort_flag:
@@ -1194,7 +1196,7 @@ class Geometry(object):
             return 0
             return 0
         bounds = self.bounds()
         bounds = self.bounds()
         return bounds[2] - bounds[0], bounds[3] - bounds[1]
         return bounds[2] - bounds[0], bounds[3] - bounds[1]
-        
+
     def get_empty_area(self, boundary=None):
     def get_empty_area(self, boundary=None):
         """
         """
         Returns the complement of self.solid_geometry within
         Returns the complement of self.solid_geometry within
@@ -1886,6 +1888,7 @@ class Geometry(object):
         # ## Index first and last points in paths
         # ## Index first and last points in paths
         def get_pts(o):
         def get_pts(o):
             return [o.coords[0], o.coords[-1]]
             return [o.coords[0], o.coords[-1]]
+
         #
         #
         # storage = FlatCAMRTreeStorage()
         # storage = FlatCAMRTreeStorage()
         # storage.get_points = get_pts
         # storage.get_points = get_pts
@@ -1982,10 +1985,10 @@ class Geometry(object):
         the geometry appropriately. This call ``scale()``. Don't call
         the geometry appropriately. This call ``scale()``. Don't call
         it again in descendents.
         it again in descendents.
 
 
-        :param units: "IN" or "MM"
-        :type units: str
-        :return: Scaling factor resulting from unit change.
-        :rtype: float
+        :param obj_units:   "IN" or "MM"
+        :type units:        str
+        :return:            Scaling factor resulting from unit change.
+        :rtype:             float
         """
         """
 
 
         if obj_units.upper() == self.units.upper():
         if obj_units.upper() == self.units.upper():
@@ -2013,8 +2016,8 @@ class Geometry(object):
         Returns a representation of the object as a dictionary.
         Returns a representation of the object as a dictionary.
         Attributes to include are listed in ``self.ser_attrs``.
         Attributes to include are listed in ``self.ser_attrs``.
 
 
-        :return: A dictionary-encoded copy of the object.
-        :rtype: dict
+        :return:    A dictionary-encoded copy of the object.
+        :rtype:     dict
         """
         """
         d = {}
         d = {}
         for attr in self.ser_attrs:
         for attr in self.ser_attrs:
@@ -2030,9 +2033,9 @@ class Geometry(object):
         be present. Use only for deserializing saved
         be present. Use only for deserializing saved
         objects.
         objects.
 
 
-        :param d: Dictionary of attributes to set in the object.
-        :type d: dict
-        :return: None
+        :param d:   Dictionary of attributes to set in the object.
+        :type d:    dict
+        :return:    None
         """
         """
         for attr in self.ser_attrs:
         for attr in self.ser_attrs:
             setattr(self, attr, d[attr])
             setattr(self, attr, d[attr])
@@ -2432,7 +2435,7 @@ class CNCjob(Geometry):
                  pp_geometry_name='default', pp_excellon_name='default',
                  pp_geometry_name='default', pp_excellon_name='default',
                  depthpercut=0.1, z_pdepth=-0.02,
                  depthpercut=0.1, z_pdepth=-0.02,
                  spindlespeed=None, spindledir='CW', dwell=True, dwelltime=1000,
                  spindlespeed=None, spindledir='CW', dwell=True, dwelltime=1000,
-                 toolchangez=0.787402, toolchange_xy=[0.0, 0.0],
+                 toolchangez=0.787402, toolchange_xy='0.0,0.0',
                  endz=2.0, endxy='',
                  endz=2.0, endxy='',
                  segx=None,
                  segx=None,
                  segy=None,
                  segy=None,
@@ -2441,7 +2444,8 @@ class CNCjob(Geometry):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
         # Used when parsing G-code arcs
         # Used when parsing G-code arcs
-        self.steps_per_circle = int(self.app.defaults['cncjob_steps_per_circle'])
+        self.steps_per_circle = steps_per_circle if steps_per_circle is not None else \
+            int(self.app.defaults['cncjob_steps_per_circle'])
 
 
         Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
         Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
 
 
@@ -2667,7 +2671,11 @@ class CNCjob(Geometry):
             if self.xy_toolchange == '':
             if self.xy_toolchange == '':
                 self.xy_toolchange = None
                 self.xy_toolchange = None
             else:
             else:
-                self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",") if self.xy_toolchange != '']
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange)) if self.xy_toolchange else None
+
+                if self.xy_toolchange and self.xy_toolchange != '':
+                    self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
+
                 if self.xy_toolchange and len(self.xy_toolchange) < 2:
                 if self.xy_toolchange and len(self.xy_toolchange) < 2:
                     self.app.inform.emit('[ERROR]%s' %
                     self.app.inform.emit('[ERROR]%s' %
                                          _("The Toolchange X,Y field in Edit -> Preferences has to be "
                                          _("The Toolchange X,Y field in Edit -> Preferences has to be "
@@ -2677,7 +2685,11 @@ class CNCjob(Geometry):
             log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
             log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
             pass
             pass
 
 
-        self.xy_end = [float(eval(a)) for a in self.xy_end.split(",") if self.xy_end != '']
+        self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
+
+        if self.xy_end and self.xy_end != '':
+            self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
+
         if self.xy_end and len(self.xy_end) < 2:
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
                                                    "in the format (x, y) but now there is only one value, not two."))
                                                    "in the format (x, y) but now there is only one value, not two."))
@@ -2689,7 +2701,7 @@ class CNCjob(Geometry):
         log.debug("Creating CNC Job from Excellon...")
         log.debug("Creating CNC Job from Excellon...")
 
 
         # Tools
         # Tools
-        
+
         # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
         # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
         # so we actually are sorting the tools by diameter
         # so we actually are sorting the tools by diameter
         # sorted_tools = sorted(exobj.tools.items(), key=lambda t1: t1['C'])
         # sorted_tools = sorted(exobj.tools.items(), key=lambda t1: t1['C'])
@@ -2700,7 +2712,7 @@ class CNCjob(Geometry):
         sorted_tools = sorted(sort, key=lambda t1: t1[1])
         sorted_tools = sorted(sort, key=lambda t1: t1[1])
 
 
         if tools == "all":
         if tools == "all":
-            tools = [i[0] for i in sorted_tools]   # we get a array of ordered tools
+            tools = [i[0] for i in sorted_tools]  # we get a array of ordered tools
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
         else:
         else:
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
@@ -3101,7 +3113,7 @@ class CNCjob(Geometry):
                             raise grace
                             raise grace
 
 
                         self.tool = tool
                         self.tool = tool
-                        self.postdata['toolC']=exobj.tools[tool]["C"]
+                        self.postdata['toolC'] = exobj.tools[tool]["C"]
                         self.tooldia = exobj.tools[tool]["C"]
                         self.tooldia = exobj.tools[tool]["C"]
 
 
                         if self.use_ui:
                         if self.use_ui:
@@ -3577,7 +3589,11 @@ class CNCjob(Geometry):
         self.startz = float(startz) if startz is not None else None
         self.startz = float(startz) if startz is not None else None
         self.z_end = float(endz) if endz is not None else None
         self.z_end = float(endz) if endz is not None else None
 
 
-        self.xy_end = [float(eval(a)) for a in endxy.split(",") if endxy != '']
+        self.xy_end = re.sub('[()\[\]]', '', str(endxy)) if endxy else None
+
+        if self.xy_end and self.xy_end != '':
+            self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
+
         if self.xy_end and len(self.xy_end) < 2:
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
                                                    "in the format (x, y) but now there is only one value, not two."))
                                                    "in the format (x, y) but now there is only one value, not two."))
@@ -3595,7 +3611,11 @@ class CNCjob(Geometry):
             if toolchangexy == '':
             if toolchangexy == '':
                 self.xy_toolchange = None
                 self.xy_toolchange = None
             else:
             else:
-                self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy)) if toolchangexy else None
+
+                if self.xy_toolchange and self.xy_toolchange != '':
+                    self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
+
                 if len(self.xy_toolchange) < 2:
                 if len(self.xy_toolchange) < 2:
                     self.app.inform.emit('[ERROR]  %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
                     self.app.inform.emit('[ERROR]  %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
                                                            "in the format (x, y) \n"
                                                            "in the format (x, y) \n"
@@ -3693,7 +3713,7 @@ class CNCjob(Geometry):
 
 
         self.gcode = self.doformat(p.start_code)
         self.gcode = self.doformat(p.start_code)
 
 
-        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.feedrate_code)  # sets the feed rate
 
 
         if toolchange is False:
         if toolchange is False:
             self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
             self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
@@ -3707,19 +3727,19 @@ class CNCjob(Geometry):
             self.gcode += self.doformat(p.toolchange_code)
             self.gcode += self.doformat(p.toolchange_code)
 
 
             if 'laser' not in self.pp_geometry_name:
             if 'laser' not in self.pp_geometry_name:
-                self.gcode += self.doformat(p.spindle_code)     # Spindle start
+                self.gcode += self.doformat(p.spindle_code)  # Spindle start
             else:
             else:
                 # for laser this will disable the laser
                 # for laser this will disable the laser
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
 
 
             if self.dwell is True:
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
         else:
         else:
             if 'laser' not in self.pp_geometry_name:
             if 'laser' not in self.pp_geometry_name:
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
 
 
             if self.dwell is True:
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
 
 
         total_travel = 0.0
         total_travel = 0.0
         total_cut = 0.0
         total_cut = 0.0
@@ -3788,7 +3808,7 @@ class CNCjob(Geometry):
                 total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
                 total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
                 current_pt = geo.coords[-1]
                 current_pt = geo.coords[-1]
 
 
-                pt, geo = storage.nearest(current_pt)   # Next
+                pt, geo = storage.nearest(current_pt)  # Next
 
 
                 disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
                 if old_disp_number < disp_number <= 100:
@@ -3961,8 +3981,13 @@ class CNCjob(Geometry):
 
 
         self.startz = float(startz) if startz is not None else self.app.defaults["geometry_startz"]
         self.startz = float(startz) if startz is not None else self.app.defaults["geometry_startz"]
         self.z_end = float(endz) if endz is not None else self.app.defaults["geometry_endz"]
         self.z_end = float(endz) if endz is not None else self.app.defaults["geometry_endz"]
-        self.xy_end = endxy if endxy != '' else self.app.defaults["geometry_endxy"]
-        self.xy_end = [float(eval(a)) for a in self.xy_end.split(",") if self.xy_end != '']
+
+        self.xy_end = endxy if endxy != '' and endxy else self.app.defaults["geometry_endxy"]
+        self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
+
+        if self.xy_end is not None and self.xy_end != '':
+            self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
+
         if self.xy_end and len(self.xy_end) < 2:
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
                                                    "in the format (x, y) but now there is only one value, not two."))
                                                    "in the format (x, y) but now there is only one value, not two."))
@@ -3978,7 +4003,11 @@ class CNCjob(Geometry):
             if toolchangexy == '':
             if toolchangexy == '':
                 self.xy_toolchange = None
                 self.xy_toolchange = None
             else:
             else:
-                self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy)) if self.xy_toolchange else None
+
+                if self.xy_toolchange and self.xy_toolchange != '':
+                    self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
+
                 if len(self.xy_toolchange) < 2:
                 if len(self.xy_toolchange) < 2:
                     self.app.inform.emit(
                     self.app.inform.emit(
                         '[ERROR] %s' %
                         '[ERROR] %s' %
@@ -4085,7 +4114,7 @@ class CNCjob(Geometry):
 
 
         self.gcode = self.doformat(p.start_code)
         self.gcode = self.doformat(p.start_code)
 
 
-        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.feedrate_code)  # sets the feed rate
 
 
         if toolchange is False:
         if toolchange is False:
             self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
             self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
@@ -4099,19 +4128,19 @@ class CNCjob(Geometry):
             self.gcode += self.doformat(p.toolchange_code)
             self.gcode += self.doformat(p.toolchange_code)
 
 
             if 'laser' not in self.pp_geometry_name:
             if 'laser' not in self.pp_geometry_name:
-                self.gcode += self.doformat(p.spindle_code)     # Spindle start
+                self.gcode += self.doformat(p.spindle_code)  # Spindle start
             else:
             else:
                 # for laser this will disable the laser
                 # for laser this will disable the laser
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
 
 
             if self.dwell is True:
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
         else:
         else:
             if 'laser' not in self.pp_geometry_name:
             if 'laser' not in self.pp_geometry_name:
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
 
 
             if self.dwell is True:
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
 
 
         total_travel = 0.0
         total_travel = 0.0
         total_cut = 0.0
         total_cut = 0.0
@@ -4553,7 +4582,7 @@ class CNCjob(Geometry):
         kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
         kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
 
 
         # Results go here
         # Results go here
-        geometry = []        
+        geometry = []
 
 
         # Last known instruction
         # Last known instruction
         current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
         current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
@@ -4636,7 +4665,7 @@ class CNCjob(Geometry):
                                 kind = ['C', 'F']
                                 kind = ['C', 'F']
                                 geometry.append(
                                 geometry.append(
                                     {
                                     {
-                                        "geom": Point(current_drill_point_coords).buffer(dia/2.0).exterior,
+                                        "geom": Point(current_drill_point_coords).buffer(dia / 2.0).exterior,
                                         "kind": kind
                                         "kind": kind
                                     }
                                     }
                                 )
                                 )
@@ -4644,14 +4673,14 @@ class CNCjob(Geometry):
 
 
             if 'G' in gobj:
             if 'G' in gobj:
                 current['G'] = int(gobj['G'])
                 current['G'] = int(gobj['G'])
-                
+
             if 'X' in gobj or 'Y' in gobj:
             if 'X' in gobj or 'Y' in gobj:
                 if 'X' in gobj:
                 if 'X' in gobj:
                     x = gobj['X']
                     x = gobj['X']
                     # current['X'] = x
                     # current['X'] = x
                 else:
                 else:
                     x = current['X']
                     x = current['X']
-                
+
                 if 'Y' in gobj:
                 if 'Y' in gobj:
                     y = gobj['Y']
                     y = gobj['Y']
                 else:
                 else:
@@ -4670,7 +4699,7 @@ class CNCjob(Geometry):
                 arcdir = [None, None, "cw", "ccw"]
                 arcdir = [None, None, "cw", "ccw"]
                 if current['G'] in [2, 3]:  # arc
                 if current['G'] in [2, 3]:  # arc
                     center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
                     center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
-                    radius = np.sqrt(gobj['I']**2 + gobj['J']**2)
+                    radius = np.sqrt(gobj['I'] ** 2 + gobj['J'] ** 2)
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     stop = np.arctan2(-center[1] + y, -center[0] + x)
                     stop = np.arctan2(-center[1] + y, -center[0] + x)
                     path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
                     path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
@@ -4864,97 +4893,30 @@ class CNCjob(Geometry):
                         if geo['kind'][0] == 'C':
                         if geo['kind'][0] == 'C':
                             obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
                             obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
                                           visible=visible, layer=1)
                                           visible=visible, layer=1)
-                # current_x = gcode_parsed[0]['geom'].coords[0][0]
-                # current_y = gcode_parsed[0]['geom'].coords[0][1]
-                # old_pos = (
-                #     current_x,
-                #     current_y
-                # )
-                #
-                # for geo in gcode_parsed:
-                #     if geo['kind'][0] == 'T':
-                #         current_position = (
-                #             geo['geom'].coords[0][0] + old_pos[0],
-                #             geo['geom'].coords[0][1] + old_pos[1]
-                #         )
-                #         if current_position not in pos:
-                #             pos.append(current_position)
-                #             path_num += 1
-                #             text.append(str(path_num))
-                #
-                #         delta = (
-                #             geo['geom'].coords[-1][0] - geo['geom'].coords[0][0],
-                #             geo['geom'].coords[-1][1] - geo['geom'].coords[0][1]
-                #         )
-                #         current_position = (
-                #             current_position[0] + geo['geom'].coords[-1][0],
-                #             current_position[1] + geo['geom'].coords[-1][1]
-                #         )
-                #         if current_position not in pos:
-                #             pos.append(current_position)
-                #             path_num += 1
-                #             text.append(str(path_num))
-                #
-                #     # plot the geometry of Excellon objects
-                #     if self.origin_kind == 'excellon':
-                #         if isinstance(geo['geom'], Point):
-                #             # if geo is Point
-                #             current_position = (
-                #                 current_position[0] + geo['geom'].x,
-                #                 current_position[1] + geo['geom'].y
-                #             )
-                #             poly = Polygon(Point(current_position))
-                #         elif isinstance(geo['geom'], LineString):
-                #             # if the geos are travel lines (LineStrings)
-                #             new_line_pts = []
-                #             old_line_pos = deepcopy(current_position)
-                #             for p in list(geo['geom'].coords):
-                #                 current_position = (
-                #                     current_position[0] + p[0],
-                #                     current_position[1] + p[1]
-                #                 )
-                #                 new_line_pts.append(current_position)
-                #                 old_line_pos = p
-                #             new_line = LineString(new_line_pts)
-                #
-                #             poly = new_line.buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)
-                #             poly = poly.simplify(tool_tolerance)
-                #     else:
-                #         # plot the geometry of any objects other than Excellon
-                #         new_line_pts = []
-                #         old_line_pos = deepcopy(current_position)
-                #         for p in list(geo['geom'].coords):
-                #             current_position = (
-                #                 current_position[0] + p[0],
-                #                 current_position[1] + p[1]
-                #             )
-                #             new_line_pts.append(current_position)
-                #             old_line_pos = p
-                #         new_line = LineString(new_line_pts)
-                #
-                #         poly = new_line.buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)
-                #         poly = poly.simplify(tool_tolerance)
-                #
-                #     old_pos = deepcopy(current_position)
-                #
-                #     if kind == 'all':
-                #         obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
-                #                       visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
-                #     elif kind == 'travel':
-                #         if geo['kind'][0] == 'T':
-                #             obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0],
-                #                           visible=visible, layer=2)
-                #     elif kind == 'cut':
-                #         if geo['kind'][0] == 'C':
-                #             obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
-                #                           visible=visible, layer=1)
 
 
             try:
             try:
-                obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
-                                   font_size=self.app.defaults["cncjob_annotation_fontsize"],
-                                   color=self.app.defaults["cncjob_annotation_fontcolor"])
-            except Exception:
-                pass
+                if self.app.defaults['global_theme'] == 'white':
+                    obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
+                                       font_size=self.app.defaults["cncjob_annotation_fontsize"],
+                                       color=self.app.defaults["cncjob_annotation_fontcolor"])
+                else:
+                    # invert the color
+                    old_color = self.app.defaults["cncjob_annotation_fontcolor"].lower()
+                    new_color = ''
+                    code = {}
+                    l1 = "#;0123456789abcdef"
+                    l2 = "#;fedcba9876543210"
+                    for i in range(len(l1)):
+                        code[l1[i]] = l2[i]
+
+                    for x in range(len(old_color)):
+                        new_color += code[old_color[x]]
+
+                    obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
+                                       font_size=self.app.defaults["cncjob_annotation_fontsize"],
+                                       color=new_color)
+            except Exception as e:
+                log.debug("CNCJob.plot2() --> annotations --> %s" % str(e))
 
 
     def create_geometry(self):
     def create_geometry(self):
         self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
         self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
@@ -5592,7 +5554,7 @@ class CNCjob(Geometry):
                             new_nr = float(nr) * xfactor
                             new_nr = float(nr) * xfactor
                             # replace the updated string
                             # replace the updated string
                             line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
                             line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
-                            )
+                                                )
 
 
                 # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
                 # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
                 if header_stop is True:
                 if header_stop is True:
@@ -5993,9 +5955,9 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
         stop += 2 * np.pi
         stop += 2 * np.pi
     if direction == "cw" and stop >= start:
     if direction == "cw" and stop >= start:
         stop -= 2 * np.pi
         stop -= 2 * np.pi
-    
+
     angle = abs(stop - start)
     angle = abs(stop - start)
-        
+
     # angle = stop-start
     # angle = stop-start
     steps = max([int(np.ceil(angle / (2 * np.pi) * steps_per_circ)), 2])
     steps = max([int(np.ceil(angle / (2 * np.pi) * steps_per_circ)), 2])
     delta_angle = da_sign[direction] * angle * 1.0 / steps
     delta_angle = da_sign[direction] * angle * 1.0 / steps
@@ -6578,7 +6540,6 @@ class FlatCAMRTreeStorage(FlatCAMRTree):
         tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
         tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
 
 
-
 # class myO:
 # class myO:
 #     def __init__(self, coords):
 #     def __init__(self, coords):
 #         self.coords = coords
 #         self.coords = coords

+ 4 - 0
defaults.py

@@ -263,6 +263,10 @@ class FlatCAMDefaults:
         "excellon_tooldia": 0.8,
         "excellon_tooldia": 0.8,
         "excellon_slot_tooldia": 1.8,
         "excellon_slot_tooldia": 1.8,
         "excellon_gcode_type": "drills",
         "excellon_gcode_type": "drills",
+        "excellon_area_exclusion": False,
+        "excellon_area_shape": "polygon",
+        "excellon_area_strategy": "over",
+        "excellon_area_overz": 1.0,
 
 
         # Excellon Advanced Options
         # Excellon Advanced Options
         "excellon_offset": 0.0,
         "excellon_offset": 0.0,

+ 8 - 3
flatcamGUI/FlatCAMGUI.py

@@ -767,7 +767,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.setCentralWidget(self.splitter)
         self.setCentralWidget(self.splitter)
 
 
         # self.notebook = QtWidgets.QTabWidget()
         # self.notebook = QtWidgets.QTabWidget()
-        self.notebook = FCDetachableTab(protect=True)
+        self.notebook = FCDetachableTab(protect=True, parent=self)
         self.notebook.setTabsClosable(False)
         self.notebook.setTabsClosable(False)
         self.notebook.useOldIndex(True)
         self.notebook.useOldIndex(True)
 
 
@@ -1174,7 +1174,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################################################################
         # ########################## PLOT AREA Tab # #############################
         # ########################## PLOT AREA Tab # #############################
         # ########################################################################
         # ########################################################################
-        self.plot_tab_area = FCDetachableTab2(protect=False, protect_by_name=[_('Plot Area')])
+        self.plot_tab_area = FCDetachableTab2(protect=False, protect_by_name=[_('Plot Area')], parent=self)
         self.plot_tab_area.useOldIndex(True)
         self.plot_tab_area.useOldIndex(True)
 
 
         self.right_lay.addWidget(self.plot_tab_area)
         self.right_lay.addWidget(self.plot_tab_area)
@@ -1372,7 +1372,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.sh_tab_layout.addLayout(self.sh_hlay)
         self.sh_tab_layout.addLayout(self.sh_hlay)
 
 
         self.app_sh_msg = (
         self.app_sh_msg = (
-            '''<b>General Shortcut list</b><br>
+            '''<b>%s</b><br>
             <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
             <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
                 <tbody>
                 <tbody>
                     <tr height="20">
                     <tr height="20">
@@ -1716,6 +1716,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             </table>
             </table>
             ''' %
             ''' %
             (
             (
+                _("General Shortcut list"),
                 _("SHOW SHORTCUT LIST"), _("Switch to Project Tab"), _("Switch to Selected Tab"),
                 _("SHOW SHORTCUT LIST"), _("Switch to Project Tab"), _("Switch to Selected Tab"),
                 _("Switch to Tool Tab"),
                 _("Switch to Tool Tab"),
                 _("New Gerber"), _("Edit Object (if selected)"), _("Grid On/Off"), _("Jump to Coordinates"),
                 _("New Gerber"), _("Edit Object (if selected)"), _("Grid On/Off"), _("Jump to Coordinates"),
@@ -2446,6 +2447,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grid_snap_btn.triggered.connect(self.on_grid_snap_triggered)
         self.grid_snap_btn.triggered.connect(self.on_grid_snap_triggered)
         self.snap_infobar_label.clicked.connect(self.on_grid_icon_snap_clicked)
         self.snap_infobar_label.clicked.connect(self.on_grid_icon_snap_clicked)
 
 
+        # to be used in the future
+        # self.plot_tab_area.tab_attached.connect(lambda x: print(x))
+        # self.plot_tab_area.tab_detached.connect(lambda x: print(x))
+
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%% GUI Building FINISHED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%% GUI Building FINISHED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

+ 22 - 5
flatcamGUI/GUIElements.py

@@ -20,6 +20,7 @@ from copy import copy
 import re
 import re
 import logging
 import logging
 import html
 import html
+import sys
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -1336,7 +1337,8 @@ class FCComboBox(QtWidgets.QComboBox):
         return str(self.currentText())
         return str(self.currentText())
 
 
     def set_value(self, val):
     def set_value(self, val):
-        self.setCurrentIndex(self.findText(str(val)))
+        idx = self.findText(str(val))
+        self.setCurrentIndex(idx)
 
 
     @property
     @property
     def is_last(self):
     def is_last(self):
@@ -1480,9 +1482,11 @@ class FCDetachableTab(QtWidgets.QTabWidget):
     From here:
     From here:
     https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
     https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
     """
     """
+    tab_detached = QtCore.pyqtSignal(str)
+    tab_attached = QtCore.pyqtSignal(str)
 
 
     def __init__(self, protect=None, protect_by_name=None, parent=None):
     def __init__(self, protect=None, protect_by_name=None, parent=None):
-        super().__init__()
+        super().__init__(parent=parent)
 
 
         self.tabBar = self.FCTabBar(self)
         self.tabBar = self.FCTabBar(self)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
@@ -1619,7 +1623,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         self.insertTab(toIndex, widget, icon, text)
         self.insertTab(toIndex, widget, icon, text)
         self.setCurrentIndex(toIndex)
         self.setCurrentIndex(toIndex)
 
 
-    @pyqtSlot(int, QtCore.QPoint)
+    # @pyqtSlot(int, QtCore.QPoint)
     def detachTab(self, index, point):
     def detachTab(self, index, point):
         """
         """
         Detach the tab by removing it's contents and placing them in
         Detach the tab by removing it's contents and placing them in
@@ -1656,6 +1660,8 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         # Create a reference to maintain access to the detached tab
         # Create a reference to maintain access to the detached tab
         self.detachedTabs[name] = detachedTab
         self.detachedTabs[name] = detachedTab
 
 
+        self.tab_detached.emit(name)
+
     def attachTab(self, contentWidget, name, icon, insertAt=None):
     def attachTab(self, contentWidget, name, icon, insertAt=None):
         """
         """
         Re-attach the tab by removing the content from the DetachedTab window,
         Re-attach the tab by removing the content from the DetachedTab window,
@@ -1668,11 +1674,11 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         :return:
         :return:
         """
         """
 
 
+        old_name = name
+
         # Make the content widget a child of this widget
         # Make the content widget a child of this widget
         contentWidget.setParent(self)
         contentWidget.setParent(self)
 
 
-        # Remove the reference
-        del self.detachedTabs[name]
         # make sure that we strip the 'FlatCAM' part of the detached name otherwise the tab name will be too long
         # make sure that we strip the 'FlatCAM' part of the detached name otherwise the tab name will be too long
         name = name.partition(' ')[2]
         name = name.partition(' ')[2]
 
 
@@ -1712,6 +1718,9 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             else:
             else:
                 index = self.insertTab(insert_index, contentWidget, icon, name)
                 index = self.insertTab(insert_index, contentWidget, icon, name)
 
 
+        obj_name = contentWidget.objectName()
+        self.tab_attached.emit(obj_name)
+
         # on reattaching the tab if protect is true then the closure button is not added
         # on reattaching the tab if protect is true then the closure button is not added
         if self.protect_tab is True:
         if self.protect_tab is True:
             self.protectTab(index)
             self.protectTab(index)
@@ -1727,6 +1736,14 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             if index > -1:
             if index > -1:
                 self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
                 self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
 
 
+        # Remove the reference
+        # Unix-like OS's crash with segmentation fault after this. FOr whatever reason, they loose reference
+        if sys.platform == 'win32':
+            try:
+                del self.detachedTabs[old_name]
+            except KeyError:
+                pass
+
     def removeTabByName(self, name):
     def removeTabByName(self, name):
         """
         """
         Remove the tab with the given name, even if it is detached
         Remove the tab with the given name, even if it is detached

+ 82 - 3
flatcamGUI/ObjectUI.py

@@ -1292,10 +1292,89 @@ class ExcellonObjectUI(ObjectUI):
         self.grid5.addWidget(pp_geo_label, 16, 0)
         self.grid5.addWidget(pp_geo_label, 16, 0)
         self.grid5.addWidget(self.pp_geo_name_cb, 16, 1)
         self.grid5.addWidget(self.pp_geo_name_cb, 16, 1)
 
 
+        # Exclusion Areas
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        self.grid5.addWidget(self.exclusion_cb, 17, 0, 1, 2)
+
+        # ------------------------------------------------------------------------------------------------------------
+        # ------------------------- EXCLUSION AREAS ------------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+        self.exclusion_frame = QtWidgets.QFrame()
+        self.exclusion_frame.setContentsMargins(0, 0, 0, 0)
+        self.grid5.addWidget(self.exclusion_frame, 18, 0, 1, 2)
+
+        self.exclusion_box = QtWidgets.QVBoxLayout()
+        self.exclusion_box.setContentsMargins(0, 0, 0, 0)
+        self.exclusion_frame.setLayout(self.exclusion_box)
+
+        h_lay = QtWidgets.QHBoxLayout()
+        self.exclusion_box.addLayout(h_lay)
+
+        # Button Add Area
+        self.add_area_button = QtWidgets.QPushButton(_('Add area'))
+        self.add_area_button.setToolTip(_("Add an Exclusion Area."))
+        h_lay.addWidget(self.add_area_button)
+
+        # Button Delete Area
+        self.delete_area_button = QtWidgets.QPushButton(_('Clear areas'))
+        self.delete_area_button.setToolTip(_("Delete all exclusion areas."))
+        h_lay.addWidget(self.delete_area_button)
+
+        grid_l = QtWidgets.QGridLayout()
+        grid_l.setColumnStretch(0, 0)
+        grid_l.setColumnStretch(1, 1)
+        self.exclusion_box.addLayout(grid_l)
+
+        # Area Selection shape
+        self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid_l.addWidget(self.area_shape_label, 0, 0)
+        grid_l.addWidget(self.area_shape_radio, 0, 1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid_l.addWidget(self.strategy_label, 1, 0)
+        grid_l.addWidget(self.strategy_radio, 1, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 9999.9999)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid_l.addWidget(self.over_z_label, 2, 0)
+        grid_l.addWidget(self.over_z_entry, 2, 1)
+
+        # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+        self.ois_exclusion_geo = OptionalHideInputSection(self.exclusion_cb, [self.exclusion_frame])
+
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.grid5.addWidget(separator_line, 17, 0, 1, 2)
+        self.grid5.addWidget(separator_line, 19, 0, 1, 2)
 
 
         # #################################################################
         # #################################################################
         # ################# GRID LAYOUT 6   ###############################
         # ################# GRID LAYOUT 6   ###############################
@@ -2025,7 +2104,7 @@ class GeometryObjectUI(ObjectUI):
         # grid4.addWidget(QtWidgets.QLabel(''), 12, 0, 1, 2)
         # grid4.addWidget(QtWidgets.QLabel(''), 12, 0, 1, 2)
 
 
         # Exclusion Areas
         # Exclusion Areas
-        self.exclusion_cb = FCCheckBox('%s:' % _("Exclusion areas"))
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
         self.exclusion_cb.setToolTip(
         self.exclusion_cb.setToolTip(
             _(
             _(
                 "Include exclusion areas.\n"
                 "Include exclusion areas.\n"
@@ -2101,7 +2180,7 @@ class GeometryObjectUI(ObjectUI):
 
 
         # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
         # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
         # ------------------------------------------------------------------------------------------------------------
         # ------------------------------------------------------------------------------------------------------------
-        self.ois_exclusion_geo = OptionalInputSection(self.exclusion_cb, [self.exclusion_frame])
+        self.ois_exclusion_geo = OptionalHideInputSection(self.exclusion_cb, [self.exclusion_frame])
 
 
         warning_lbl = QtWidgets.QLabel(
         warning_lbl = QtWidgets.QLabel(
             _(
             _(

+ 2 - 2
flatcamGUI/PlotCanvasLegacy.py

@@ -948,10 +948,10 @@ class ShapeCollectionLegacy:
         """
         """
 
 
         :param obj:             This is the object to which the shapes collection is attached and for
         :param obj:             This is the object to which the shapes collection is attached and for
-        which it will have to draw shapes
+                                which it will have to draw shapes
         :param app:             This is the FLatCAM.App usually, needed because we have to access attributes there
         :param app:             This is the FLatCAM.App usually, needed because we have to access attributes there
         :param name:            This is the name given to the Matplotlib axes; it needs to be unique due of
         :param name:            This is the name given to the Matplotlib axes; it needs to be unique due of
-        Matplotlib requurements
+                                Matplotlib requurements
         :param annotation_job:  Make this True if the job needed is just for annotation
         :param annotation_job:  Make this True if the job needed is just for annotation
         :param linewidth:       THe width of the line (outline where is the case)
         :param linewidth:       THe width of the line (outline where is the case)
         """
         """

+ 12 - 5
flatcamGUI/preferences/general/GeneralGUIPrefGroupUI.py

@@ -420,13 +420,20 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
 
 
     def on_theme_change(self):
     def on_theme_change(self):
         val = self.theme_radio.get_value()
         val = self.theme_radio.get_value()
-        qsettings = QSettings("Open Source", "FlatCAM")
-        qsettings.setValue('theme', val)
 
 
-        # This will write the setting to the platform specific storage.
-        del qsettings
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if val != theme:
+            theme_settings.setValue('theme', val)
+
+            # This will write the setting to the platform specific storage.
+            del theme_settings
 
 
-        self.app.on_app_restart()
+            self.app.on_app_restart()
 
 
     @staticmethod
     @staticmethod
     def handle_style(style):
     def handle_style(style):

+ 24 - 1
flatcamObjects/FlatCAMExcellon.py

@@ -567,7 +567,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
             "ppname_g": self.ui.pp_geo_name_cb,
             "ppname_g": self.ui.pp_geo_name_cb,
             "z_pdepth": self.ui.pdepth_entry,
             "z_pdepth": self.ui.pdepth_entry,
             "feedrate_probe": self.ui.feedrate_probe_entry,
             "feedrate_probe": self.ui.feedrate_probe_entry,
-            # "gcode_type": self.ui.excellon_gcode_type_radio
+            # "gcode_type": self.ui.excellon_gcode_type_radio,
+            "area_exclusion": self.ui.exclusion_cb,
+            "area_shape": self.ui.area_shape_radio,
+            "area_strategy": self.ui.strategy_radio,
+            "area_overz": self.ui.over_z_entry,
         })
         })
 
 
         self.name2option = {
         self.name2option = {
@@ -634,6 +638,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
         self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
         self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click)
         self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click)
 
 
+        self.ui.add_area_button.clicked.connect(self.on_add_area_click)
+        self.ui.delete_area_button.clicked.connect(self.on_clear_area_click)
+
         self.on_operation_type(val='drill')
         self.on_operation_type(val='drill')
         self.ui.operation_radio.activated_custom.connect(self.on_operation_type)
         self.ui.operation_radio.activated_custom.connect(self.on_operation_type)
 
 
@@ -1115,6 +1122,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     else:
                     else:
                         geo_obj.solid_geometry.append(
                         geo_obj.solid_geometry.append(
                             Point(hole['point']).buffer(buffer_value).exterior)
                             Point(hole['point']).buffer(buffer_value).exterior)
+
         if use_thread:
         if use_thread:
             def geo_thread(app_obj):
             def geo_thread(app_obj):
                 app_obj.new_object("geometry", outname, geo_init, plot=plot)
                 app_obj.new_object("geometry", outname, geo_init, plot=plot)
@@ -1466,6 +1474,21 @@ class ExcellonObject(FlatCAMObj, Excellon):
         #     self.options['startz'] = float(self.options['startz']) * factor
         #     self.options['startz'] = float(self.options['startz']) * factor
         # self.options['endz'] = float(self.options['endz']) * factor
         # self.options['endz'] = float(self.options['endz']) * factor
 
 
+    def on_add_area_click(self):
+        shape_button = self.ui.area_shape_radio
+        overz_button = self.ui.over_z_entry
+        strategy_radio = self.ui.strategy_radio
+        cnc_button = self.ui.generate_cnc_button
+        solid_geo = self.solid_geometry
+        obj_type = self.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        self.app.exc_areas.on_clear_area_click()
+
     def on_solid_cb_click(self, *args):
     def on_solid_cb_click(self, *args):
         if self.muted_ui:
         if self.muted_ui:
             return
             return

+ 26 - 244
flatcamObjects/FlatCAMGeometry.py

@@ -16,7 +16,6 @@ import shapely.affinity as affinity
 from camlib import Geometry
 from camlib import Geometry
 
 
 from flatcamObjects.FlatCAMObj import *
 from flatcamObjects.FlatCAMObj import *
-import FlatCAMTool
 
 
 import ezdxf
 import ezdxf
 import math
 import math
@@ -151,18 +150,6 @@ class GeometryObject(FlatCAMObj, Geometry):
 
 
         self.param_fields = {}
         self.param_fields = {}
 
 
-        # Event signals disconnect id holders
-        self.mr = None
-        self.mm = None
-        self.kp = None
-
-        # variables to be used in area exclusion
-        self.cursor_pos = (0, 0)
-        self.exclusion_areas_list = []
-        self.first_click = False
-        self.points = []
-        self.poly_drawn = False
-
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
@@ -363,7 +350,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             "endxy": self.ui.endxy_entry,
             "endxy": self.ui.endxy_entry,
             "cnctooldia": self.ui.addtool_entry,
             "cnctooldia": self.ui.addtool_entry,
             "area_exclusion": self.ui.exclusion_cb,
             "area_exclusion": self.ui.exclusion_cb,
-            "area_shape":self.ui.area_shape_radio,
+            "area_shape": self.ui.area_shape_radio,
             "area_strategy": self.ui.strategy_radio,
             "area_strategy": self.ui.strategy_radio,
             "area_overz": self.ui.over_z_entry,
             "area_overz": self.ui.over_z_entry,
         })
         })
@@ -1149,8 +1136,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
-                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'"
-                )
+                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
             )
             )
             self.ui.cutz_entry.setToolTip(
             self.ui.cutz_entry.setToolTip(
                 _("Disabled because the tool is V-shape.\n"
                 _("Disabled because the tool is V-shape.\n"
@@ -1159,8 +1145,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
-                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'"
-                  )
+                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
             )
             )
 
 
             self.update_cutz()
             self.update_cutz()
@@ -1172,8 +1157,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.ui.cutz_entry.setDisabled(False)
             self.ui.cutz_entry.setDisabled(False)
             self.ui.cutzlabel.setToolTip(
             self.ui.cutzlabel.setToolTip(
                 _("Cutting depth (negative)\n"
                 _("Cutting depth (negative)\n"
-                  "below the copper surface."
-                )
+                  "below the copper surface.")
             )
             )
             self.ui.cutz_entry.setToolTip('')
             self.ui.cutz_entry.setToolTip('')
 
 
@@ -1690,15 +1674,11 @@ class GeometryObject(FlatCAMObj, Geometry):
         The actual work is done by the target CNCJobObject object's
         The actual work is done by the target CNCJobObject object's
         `generate_from_geometry_2()` method.
         `generate_from_geometry_2()` method.
 
 
-        :param tools_dict: a dictionary that holds the whole data needed to create the Gcode
-        (including the solid_geometry)
-
-        :param tools_in_use: the tools that are used, needed by some preprocessors
-        :type list of lists, each list in the list is made out of row elements of tools table from GUI
-
         :param outname:
         :param outname:
-        :param tools_dict:
-        :param tools_in_use:
+        :param tools_dict:      a dictionary that holds the whole data needed to create the Gcode
+                                (including the solid_geometry)
+        :param tools_in_use:    the tools that are used, needed by some preprocessors
+        :type  tools_in_use     list of lists, each list in the list is made out of row elements of tools table from GUI
         :param segx:            number of segments on the X axis, for auto-levelling
         :param segx:            number of segments on the X axis, for auto-levelling
         :param segy:            number of segments on the Y axis, for auto-levelling
         :param segy:            number of segments on the Y axis, for auto-levelling
         :param plot:            if True the generated object will be plotted; if False will not be plotted
         :param plot:            if True the generated object will be plotted; if False will not be plotted
@@ -1742,7 +1722,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # count the tools
             # count the tools
             tool_cnt = 0
             tool_cnt = 0
 
 
-            dia_cnc_dict = {}
+            # dia_cnc_dict = {}
 
 
             # this turn on the FlatCAMCNCJob plot for multiple tools
             # this turn on the FlatCAMCNCJob plot for multiple tools
             job_obj.multitool = True
             job_obj.multitool = True
@@ -1882,7 +1862,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # count the tools
             # count the tools
             tool_cnt = 0
             tool_cnt = 0
 
 
-            dia_cnc_dict = {}
+            # dia_cnc_dict = {}
 
 
             # this turn on the FlatCAMCNCJob plot for multiple tools
             # this turn on the FlatCAMCNCJob plot for multiple tools
             job_obj.multitool = True
             job_obj.multitool = True
@@ -2056,7 +2036,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             use_thread=True,
             use_thread=True,
             plot=True):
             plot=True):
         """
         """
-        Only used for TCL Command.
+        Only used by the TCL Command Cncjob.
         Creates a CNCJob out of this Geometry object. The actual
         Creates a CNCJob out of this Geometry object. The actual
         work is done by the target camlib.CNCjob
         work is done by the target camlib.CNCjob
         `generate_from_geometry_2()` method.
         `generate_from_geometry_2()` method.
@@ -2469,6 +2449,21 @@ class GeometryObject(FlatCAMObj, Geometry):
 
 
         return factor
         return factor
 
 
+    def on_add_area_click(self):
+        shape_button = self.ui.area_shape_radio
+        overz_button = self.ui.over_z_entry
+        strategy_radio = self.ui.strategy_radio
+        cnc_button = self.ui.generate_cnc_button
+        solid_geo = self.solid_geometry
+        obj_type = self.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        self.app.exc_areas.on_clear_area_click()
+
     def plot_element(self, element, color=None, visible=None):
     def plot_element(self, element, color=None, visible=None):
 
 
         if color is None:
         if color is None:
@@ -2573,219 +2568,6 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.ui.plot_cb.setChecked(True)
             self.ui.plot_cb.setChecked(True)
         self.ui_connect()
         self.ui_connect()
 
 
-    def on_add_area_click(self):
-        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
-        self.app.call_source = 'geometry'
-
-        if self.app.is_legacy is False:
-            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-        else:
-            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
-            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
-            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
-
-        self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
-        self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
-        # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
-
-    # To be called after clicking on the plot.
-    def on_mouse_release(self, event):
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            # event_is_dragging = event.is_dragging
-            right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            # event_is_dragging = self.app.plotcanvas.is_dragging
-            right_button = 3
-
-        event_pos = self.app.plotcanvas.translate_coords(event_pos)
-        if self.app.grid_status():
-            curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-        else:
-            curr_pos = (event_pos[0], event_pos[1])
-
-        x1, y1 = curr_pos[0], curr_pos[1]
-
-        shape_type = self.ui.area_shape_radio.get_value()
-
-        # do clear area only for left mouse clicks
-        if event.button == 1:
-            if shape_type == "square":
-                if self.first_click is False:
-                    self.first_click = True
-                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
-
-                    self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
-                    if self.app.grid_status():
-                        self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-                else:
-                    self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
-                    self.app.delete_selection_shape()
-
-                    x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
-
-                    pt1 = (x0, y0)
-                    pt2 = (x1, y0)
-                    pt3 = (x1, y1)
-                    pt4 = (x0, y1)
-
-                    new_rectangle = Polygon([pt1, pt2, pt3, pt4])
-                    self.exclusion_areas_list.append(new_rectangle)
-
-                    # add a temporary shape on canvas
-                    FlatCAMTool.FlatCAMTool.draw_tool_selection_shape(self, old_coords=(x0, y0), coords=(x1, y1))
-
-                    self.first_click = False
-                    return
-            else:
-                self.points.append((x1, y1))
-
-                if len(self.points) > 1:
-                    self.poly_drawn = True
-                    self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
-
-                return ""
-        elif event.button == right_button and self.mouse_is_dragging is False:
-
-            shape_type = self.ui.area_shape_radio.get_value()
-
-            if shape_type == "square":
-                self.first_click = False
-            else:
-                # if we finish to add a polygon
-                if self.poly_drawn is True:
-                    try:
-                        # try to add the point where we last clicked if it is not already in the self.points
-                        last_pt = (x1, y1)
-                        if last_pt != self.points[-1]:
-                            self.points.append(last_pt)
-                    except IndexError:
-                        pass
-
-                    # we need to add a Polygon and a Polygon can be made only from at least 3 points
-                    if len(self.points) > 2:
-                        FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-                        pol = Polygon(self.points)
-                        # do not add invalid polygons even if they are drawn by utility geometry
-                        if pol.is_valid:
-                            self.exclusion_areas_list.append(pol)
-                            FlatCAMTool.FlatCAMTool.draw_selection_shape_polygon(self, points=self.points)
-                            self.app.inform.emit(
-                                _("Zone added. Click to start adding next zone or right click to finish."))
-
-                    self.points = []
-                    self.poly_drawn = False
-                    return
-
-            FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self)
-
-            if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
-                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
-                # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.mr)
-                self.app.plotcanvas.graph_event_disconnect(self.mm)
-                # self.app.plotcanvas.graph_event_disconnect(self.kp)
-
-            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
-                                                                  self.app.on_mouse_click_over_plot)
-            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
-                                                                  self.app.on_mouse_move_over_plot)
-            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                                  self.app.on_mouse_click_release_over_plot)
-
-            self.app.call_source = 'app'
-
-            if len(self.exclusion_areas_list) == 0:
-                return
-
-    def area_disconnect(self):
-        if self.app.is_legacy is False:
-            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
-            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
-        else:
-            self.app.plotcanvas.graph_event_disconnect(self.mr)
-            self.app.plotcanvas.graph_event_disconnect(self.mm)
-            self.app.plotcanvas.graph_event_disconnect(self.kp)
-
-        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
-                                                              self.app.on_mouse_click_over_plot)
-        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
-                                                              self.app.on_mouse_move_over_plot)
-        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                              self.app.on_mouse_click_release_over_plot)
-        self.points = []
-        self.poly_drawn = False
-        self.exclusion_areas_list = []
-
-        FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-        FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self)
-
-        self.app.call_source = "app"
-        self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
-
-    # called on mouse move
-    def on_mouse_move(self, event):
-        shape_type = self.ui.area_shape_radio.get_value()
-
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            event_is_dragging = event.is_dragging
-            # right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            event_is_dragging = self.app.plotcanvas.is_dragging
-            # right_button = 3
-
-        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
-
-        # detect mouse dragging motion
-        if event_is_dragging is True:
-            self.mouse_is_dragging = True
-        else:
-            self.mouse_is_dragging = False
-
-        # update the cursor position
-        if self.app.grid_status():
-            # Update cursor
-            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
-
-            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
-                                         symbol='++', edge_color=self.app.cursor_color_3D,
-                                         edge_width=self.app.defaults["global_cursor_width"],
-                                         size=self.app.defaults["global_cursor_size"])
-
-        # update the positions on status bar
-        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
-        if self.cursor_pos is None:
-            self.cursor_pos = (0, 0)
-
-        self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
-        self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
-        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
-
-        # draw the utility geometry
-        if shape_type == "square":
-            if self.first_click:
-                self.app.delete_selection_shape()
-                self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
-                                                     coords=(curr_pos[0], curr_pos[1]))
-        else:
-            FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-            FlatCAMTool.FlatCAMTool.draw_moving_selection_shape_poly(
-                self, points=self.points, data=(curr_pos[0], curr_pos[1]))
-
-    def on_clear_area_click(self):
-        self.exclusion_areas_list = []
-        FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-        self.app.delete_selection_shape()
-
     @staticmethod
     @staticmethod
     def merge(geo_list, geo_final, multigeo=None):
     def merge(geo_list, geo_final, multigeo=None):
         """
         """

+ 17 - 8
flatcamTools/ToolNCC.py

@@ -2066,6 +2066,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
             # unfortunately for this function to work time efficient,
             # unfortunately for this function to work time efficient,
             # if the Gerber was loaded without buffering then it require the buffering now.
             # if the Gerber was loaded without buffering then it require the buffering now.
+            # TODO 'buffering status' should be a property of the object not the project property
             if self.app.defaults['gerber_buffering'] == 'no':
             if self.app.defaults['gerber_buffering'] == 'no':
                 self.solid_geometry = ncc_obj.solid_geometry.buffer(0)
                 self.solid_geometry = ncc_obj.solid_geometry.buffer(0)
             else:
             else:
@@ -2158,6 +2159,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
                 self.app.inform.emit('[success] %s ...' % _("Buffering finished"))
                 self.app.inform.emit('[success] %s ...' % _("Buffering finished"))
+
             empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
             empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
             if empty == 'fail':
             if empty == 'fail':
                 return 'fail'
                 return 'fail'
@@ -2203,14 +2205,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
         """
         """
         Clear the excess copper from the entire object.
         Clear the excess copper from the entire object.
 
 
-        :param ncc_obj: ncc cleared object
+        :param ncc_obj:         ncc cleared object
         :param sel_obj:
         :param sel_obj:
-        :param ncctooldia: a tuple or single element made out of diameters of the tools to be used to ncc clear
-        :param isotooldia: a tuple or single element made out of diameters of the tools to be used for isolation
-        :param outname: name of the resulting object
-        :param order:
-        :param tools_storage: whether to use the current tools_storage self.ncc_tools or a different one.
-        Usage of the different one is related to when this function is called from a TcL command.
+        :param ncctooldia:      a tuple or single element made out of diameters of the tools to be used to ncc clear
+        :param isotooldia:      a tuple or single element made out of diameters of the tools to be used for isolation
+        :param outname:         name of the resulting object
+        :param order:           Tools order
+        :param tools_storage:   whether to use the current tools_storage self.ncc_tools or a different one.
+                                Usage of the different one is related to when this function is called
+                                from a TcL command.
 
 
         :param run_threaded: If True the method will be run in a threaded way suitable for GUI usage; if False it will
         :param run_threaded: If True the method will be run in a threaded way suitable for GUI usage; if False it will
         run non-threaded for TclShell usage
         run non-threaded for TclShell usage
@@ -3870,6 +3873,11 @@ class NonCopperClear(FlatCAMTool, Gerber):
         Returns the complement of target geometry within
         Returns the complement of target geometry within
         the given boundary polygon. If not specified, it defaults to
         the given boundary polygon. If not specified, it defaults to
         the rectangular bounding box of target geometry.
         the rectangular bounding box of target geometry.
+
+        :param target:      The geometry that is to be 'inverted'
+        :param boundary:    A polygon that surrounds the entire solid geometry and from which we subtract in order to
+                            create a "negative" geometry (geometry to be emptied of copper)
+        :return:
         """
         """
         if isinstance(target, Polygon):
         if isinstance(target, Polygon):
             geo_len = 1
             geo_len = 1
@@ -3882,6 +3890,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             boundary = target.envelope
             boundary = target.envelope
         else:
         else:
             boundary = boundary
             boundary = boundary
+
         try:
         try:
             ret_val = boundary.difference(target)
             ret_val = boundary.difference(target)
         except Exception:
         except Exception:
@@ -3889,10 +3898,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 for el in target:
                 for el in target:
                     # provide the app with a way to process the GUI events when in a blocking loop
                     # provide the app with a way to process the GUI events when in a blocking loop
                     QtWidgets.QApplication.processEvents()
                     QtWidgets.QApplication.processEvents()
-
                     if self.app.abort_flag:
                     if self.app.abort_flag:
                         # graceful abort requested by the user
                         # graceful abort requested by the user
                         raise grace
                         raise grace
+
                     boundary = boundary.difference(el)
                     boundary = boundary.difference(el)
                     pol_nr += 1
                     pol_nr += 1
                     disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                     disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))