ソースを参照

- changed how the import of svg.path module is done in the ParseSVG.py file
- Tool Isolation - new feature that allow to isolate interiors of polygons (holes in polygons). It is possible that the isolation to be reported as successful (internal limitations) but some interiors to not be isolated. This way the user get to fix the isolation by doing an extra isolation.

Marius Stanciu 5 年 前
コミット
2107a4766f

+ 5 - 3
CHANGELOG.md

@@ -10,6 +10,11 @@ CHANGELOG for FlatCAM beta
 5.06.2020
 
 - fixed a small issue in the Panelization Tool that blocked the usage of a Geometry object as panelization reference
+- in Tool Calculators fixed an application crash if the user typed letters instead of numbers in the boxes. Now the boxes accept only numbers, dots, comma, spaces and arithmetic operators
+- NumericalEvalEntry allow the input of commas now
+- Tool Calculators: allowed comma to be used as decimal separator
+- changed how the import of svg.path module is done in the ParseSVG.py file
+- Tool Isolation - new feature that allow to isolate interiors of polygons (holes in polygons). It is possible that the isolation to be reported as successful (internal limitations) but some interiors to not be isolated. This way the user get to fix the isolation by doing an extra isolation.
 
 4.06.2020
 
@@ -122,9 +127,6 @@ CHANGELOG for FlatCAM beta
 27.05.2020
 
 - working on Isolation Tool: made to work the Isolation with multiple tools without rest machining
-- in Tool Calculators fixed an application crash if the user typed letters instead of numbers in the boxes. Now the boxes accept only numbers, dots, comma, spaces and arithmetic operators
-- NumericalEvalEntry allow the input of commas now
-- Tool Calculators: allowed comma to be used as decimal separator
 
 26.05.2020
 

+ 1 - 0
appGUI/preferences/PreferencesUIManager.py

@@ -348,6 +348,7 @@ class PreferencesUIManager:
             "tools_iso_combine_passes": self.ui.tools_defaults_form.tools_iso_group.combine_passes_cb,
             "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
             "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
+            "tools_iso_poly_ints":      self.ui.tools_defaults_form.tools_iso_group.poly_int_cb,
             "tools_iso_area_shape":     self.ui.tools_defaults_form.tools_iso_group.area_shape_radio,
             "tools_iso_plotting":       self.ui.tools_defaults_form.tools_iso_group.plotting_radio,
 

+ 14 - 3
appGUI/preferences/tools/ToolsISOPrefGroupUI.py

@@ -301,10 +301,21 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.area_shape_label, 21, 0)
         grid0.addWidget(self.area_shape_radio, 21, 1, 1, 2)
 
+        # Polygon interiors selection
+        self.poly_int_label = QtWidgets.QLabel('%s:' % _("Interiors"))
+        self.poly_int_label.setToolTip(
+            _("When checked the user can select interiors of a polygon.\n"
+              "(holes in the polygon).")
+        )
+        self.poly_int_cb = FCCheckBox()
+
+        grid0.addWidget(self.poly_int_label, 22, 0)
+        grid0.addWidget(self.poly_int_cb, 22, 1)
+
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 22, 0, 1, 3)
+        grid0.addWidget(separator_line, 24, 0, 1, 3)
 
         # ## Plotting type
         self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
@@ -314,7 +325,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
             _("- 'Normal' -  normal plotting, done at the end of the job\n"
               "- 'Progressive' - each shape is plotted after it is generated")
         )
-        grid0.addWidget(plotting_label, 23, 0)
-        grid0.addWidget(self.plotting_radio, 23, 1, 1, 2)
+        grid0.addWidget(plotting_label, 25, 0)
+        grid0.addWidget(self.plotting_radio, 25, 1, 1, 2)
 
         self.layout.addStretch()

+ 26 - 20
appParsers/ParseSVG.py

@@ -21,9 +21,10 @@
 
 # import xml.etree.ElementTree as ET
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
-from svg.path.path import Move
-from svg.path.path import Close
-from shapely.geometry import LineString, LinearRing, MultiLineString
+# from svg.path.path import Move
+# from svg.path.path import Close
+import svg.path
+from shapely.geometry import LineString, MultiLineString
 from shapely.affinity import skew, affine_transform, rotate
 import numpy as np
 
@@ -59,16 +60,17 @@ def path2shapely(path, object_type, res=1.0):
     Converts an svg.path.Path into a Shapely
     Polygon or LinearString.
 
-    :rtype : Polygon
-    :rtype : LineString
-    :param path: svg.path.Path instance
-    :param res: Resolution (minimum step along path)
-    :return: Shapely geometry object
+    :param path:        svg.path.Path instance
+    :param object_type:
+    :param res:         Resolution (minimum step along path)
+    :return:            Shapely geometry object
+    :rtype :            Polygon
+    :rtype :            LineString
     """
 
     points = []
     geometry = []
-    geo_element = None
+
     rings = []
     closed = False
 
@@ -111,7 +113,7 @@ def path2shapely(path, object_type, res=1.0):
             continue
 
         # Move
-        if isinstance(component, Move):
+        if isinstance(component, svg.path.Move):
             if not points:
                 continue
             else:
@@ -128,7 +130,7 @@ def path2shapely(path, object_type, res=1.0):
         closed = False
 
         # Close
-        if isinstance(component, Close):
+        if isinstance(component, svg.path.Close):
             if not points:
                 continue
             else:
@@ -176,9 +178,11 @@ def svgrect2shapely(rect, n_points=32):
     """
     Converts an SVG rect into Shapely geometry.
 
-    :param rect: Rect Element
-    :type rect: xml.etree.ElementTree.Element
-    :return: shapely.geometry.polygon.LinearRing
+    :param rect:        Rect Element
+    :type rect:         xml.etree.ElementTree.Element
+    :param n_points:    number of points to approximate circles
+    :type n_points:     int
+    :return:            shapely.geometry.polygon.LinearRing
     """
     w = svgparselength(rect.get('width'))[0]
     h = svgparselength(rect.get('height'))[0]
@@ -325,9 +329,10 @@ def getsvggeo(node, object_type, root=None):
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
 
-    :param node: xml.etree.ElementTree.Element
-    :return: List of Shapely geometry
-    :rtype: list
+    :param node:        xml.etree.ElementTree.Element
+    :param object_type:
+    :return:            List of Shapely geometry
+    :rtype:             list
     """
     if root is None:
         root = node
@@ -427,9 +432,10 @@ def getsvgtext(node, object_type, units='MM'):
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
 
-    :param node: xml.etree.ElementTree.Element
-    :return: List of Shapely geometry
-    :rtype: list
+    :param node:        xml.etree.ElementTree.Element
+    :param object_type:
+    :return:            List of Shapely geometry
+    :rtype:             list
     """
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
     geo = []

+ 224 - 128
appTools/ToolIsolation.py

@@ -18,7 +18,7 @@ import numpy as np
 import math
 
 from shapely.ops import cascaded_union
-from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing
+from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing, Point
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 
@@ -536,6 +536,20 @@ class ToolIsolation(AppTool, Gerber):
         self.reference_combo_type.hide()
         self.reference_combo_type_label.hide()
 
+        # Polygon interiors selection
+        self.poly_int_label = QtWidgets.QLabel('%s:' % _("Interiors"))
+        self.poly_int_label.setToolTip(
+            _("When checked the user can select interiors of a polygon.\n"
+              "(holes in the polygon).")
+        )
+        self.poly_int_cb = FCCheckBox()
+
+        self.grid3.addWidget(self.poly_int_label, 33, 0)
+        self.grid3.addWidget(self.poly_int_cb, 33, 1)
+
+        self.poly_int_label.hide()
+        self.poly_int_cb.hide()
+
         # Area Selection shape
         self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
         self.area_shape_label.setToolTip(
@@ -545,8 +559,8 @@ class ToolIsolation(AppTool, Gerber):
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
                                           {'label': _("Polygon"), 'value': 'polygon'}])
 
-        self.grid3.addWidget(self.area_shape_label, 33, 0)
-        self.grid3.addWidget(self.area_shape_radio, 33, 1)
+        self.grid3.addWidget(self.area_shape_label, 35, 0)
+        self.grid3.addWidget(self.area_shape_radio, 35, 1)
 
         self.area_shape_label.hide()
         self.area_shape_radio.hide()
@@ -554,7 +568,7 @@ class ToolIsolation(AppTool, Gerber):
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.grid3.addWidget(separator_line, 34, 0, 1, 2)
+        self.grid3.addWidget(separator_line, 36, 0, 1, 2)
 
         self.generate_iso_button = QtWidgets.QPushButton("%s" % _("Generate Isolation Geometry"))
         self.generate_iso_button.setStyleSheet("""
@@ -865,6 +879,7 @@ class ToolIsolation(AppTool, Gerber):
         self.milling_type_radio.set_value(self.app.defaults["tools_iso_milling_type"])
         self.combine_passes_cb.set_value(self.app.defaults["tools_iso_combine_passes"])
         self.area_shape_radio.set_value(self.app.defaults["tools_iso_area_shape"])
+        self.poly_int_cb.set_value(self.app.defaults["tools_iso_poly_ints"])
 
         self.cutz_entry.set_value(self.app.defaults["tools_iso_tool_cutz"])
         self.tool_type_radio.set_value(self.app.defaults["tools_iso_tool_type"])
@@ -1291,6 +1306,8 @@ class ToolIsolation(AppTool, Gerber):
             self.reference_combo_type_label.hide()
             self.area_shape_label.hide()
             self.area_shape_radio.hide()
+            self.poly_int_label.hide()
+            self.poly_int_cb.hide()
 
             # disable rest-machining for area painting
             self.rest_cb.setDisabled(False)
@@ -1301,6 +1318,8 @@ class ToolIsolation(AppTool, Gerber):
             self.reference_combo_type_label.hide()
             self.area_shape_label.show()
             self.area_shape_radio.show()
+            self.poly_int_label.hide()
+            self.poly_int_cb.hide()
 
             # disable rest-machining for area isolation
             self.rest_cb.set_value(False)
@@ -1312,6 +1331,8 @@ class ToolIsolation(AppTool, Gerber):
             self.reference_combo_type_label.hide()
             self.area_shape_label.hide()
             self.area_shape_radio.hide()
+            self.poly_int_label.show()
+            self.poly_int_cb.show()
         else:
             self.reference_combo.show()
             self.reference_combo_label.show()
@@ -1319,6 +1340,8 @@ class ToolIsolation(AppTool, Gerber):
             self.reference_combo_type_label.show()
             self.area_shape_label.hide()
             self.area_shape_radio.hide()
+            self.poly_int_label.hide()
+            self.poly_int_cb.hide()
 
             # disable rest-machining for area painting
             self.rest_cb.setDisabled(False)
@@ -1714,7 +1737,7 @@ class ToolIsolation(AppTool, Gerber):
             use_geo = cascaded_union(isolated_obj.solid_geometry).difference(ref_geo)
             self.isolate(isolated_obj=isolated_obj, geometry=use_geo)
 
-    def isolate(self, isolated_obj, geometry=None, limited_area=None, plot=True):
+    def isolate(self, isolated_obj, geometry=None, limited_area=None, negative_dia=None, plot=True):
         """
         Creates an isolation routing geometry object in the project.
 
@@ -1724,6 +1747,8 @@ class ToolIsolation(AppTool, Gerber):
         :type geometry:         List of Shapely polygon
         :param limited_area:    if not None isolate only this area
         :type limited_area:     Shapely Polygon or a list of them
+        :param negative_dia:    isolate the geometry with a negative value for the tool diameter
+        :type negative_dia:     bool
         :param plot:            if to plot the resulting geometry object
         :type plot:             bool
         :return: None
@@ -1745,10 +1770,10 @@ class ToolIsolation(AppTool, Gerber):
         if combine:
             if self.rest_cb.get_value():
                 self.combined_rest(iso_obj=isolated_obj, iso2geo=geometry, tools_storage=tools_storage,
-                                   lim_area=limited_area, plot=plot)
+                                   lim_area=limited_area, negative_dia=negative_dia, plot=plot)
             else:
                 self.combined_normal(iso_obj=isolated_obj, iso2geo=geometry, tools_storage=tools_storage,
-                                     lim_area=limited_area, plot=plot)
+                                     lim_area=limited_area, negative_dia=negative_dia, plot=plot)
 
         else:
             prog_plot = self.app.defaults["tools_iso_plotting"]
@@ -1780,6 +1805,9 @@ class ToolIsolation(AppTool, Gerber):
                     tool_type = tools_storage[tool]['tool_type']
 
                     iso_offset = tool_dia * ((2 * i + 1) / 2.0000001) - (i * overlap * tool_dia)
+                    if negative_dia:
+                        iso_offset = -iso_offset
+
                     outname = "%s_%.*f" % (isolated_obj.options["name"], self.decimals, float(tool_dia))
 
                     if passes > 1:
@@ -1879,7 +1907,7 @@ class ToolIsolation(AppTool, Gerber):
         # Switch notebook to Selected page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
-    def combined_rest(self, iso_obj, iso2geo, tools_storage, lim_area, plot=True):
+    def combined_rest(self, iso_obj, iso2geo, tools_storage, lim_area, negative_dia=None, plot=True):
         """
         Isolate the provided Gerber object using "rest machining" strategy
 
@@ -1891,6 +1919,8 @@ class ToolIsolation(AppTool, Gerber):
         :type tools_storage:    dict
         :param lim_area:        if not None restrict isolation to this area
         :type lim_area:         Shapely Polygon or a list of them
+        :param negative_dia:    isolate the geometry with a negative value for the tool diameter
+        :type negative_dia:     bool
         :param plot:            if to plot the resulting geometry object
         :type plot:             bool
         :return:                Isolated solid geometry
@@ -1957,7 +1987,8 @@ class ToolIsolation(AppTool, Gerber):
 
                     solid_geo, work_geo = self.generate_rest_geometry(geometry=work_geo, tooldia=tool_dia,
                                                                       passes=passes, overlap=overlap, invert=mill_dir,
-                                                                      env_iso_type=iso_t, prog_plot=prog_plot,
+                                                                      env_iso_type=iso_t, negative_dia=negative_dia,
+                                                                      prog_plot=prog_plot,
                                                                       prog_plot_handler=self.plot_temp_shapes)
 
                     # ############################################################
@@ -2050,7 +2081,7 @@ class ToolIsolation(AppTool, Gerber):
                 msg += coords
             self.app.shell_message(msg=msg)
 
-    def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, plot=True):
+    def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, negative_dia=None, plot=True):
         """
 
         :param iso_obj:         the isolated Gerber object
@@ -2061,6 +2092,8 @@ class ToolIsolation(AppTool, Gerber):
         :type tools_storage:    dict
         :param lim_area:        if not None restrict isolation to this area
         :type lim_area:         Shapely Polygon or a list of them
+        :param negative_dia:    isolate the geometry with a negative value for the tool diameter
+        :type negative_dia:     bool
         :param plot:            if to plot the resulting geometry object
         :type plot:             bool
         :return:                Isolated solid geometry
@@ -2116,6 +2149,8 @@ class ToolIsolation(AppTool, Gerber):
             solid_geo = []
             for nr_pass in range(passes):
                 iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * tool_dia)
+                if negative_dia:
+                    iso_offset = -iso_offset
 
                 # if milling type is climb then the move is counter-clockwise around features
                 mill_dir = 1 if milling_type == 'cl' else 0
@@ -2340,7 +2375,14 @@ class ToolIsolation(AppTool, Gerber):
             curr_pos = (curr_pos[0], curr_pos[1])
 
         if event.button == 1:
-            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]), geoset=self.grb_obj.solid_geometry)
+            if self.poly_int_cb.get_value() is True:
+                clicked_poly = self.find_polygon_ignore_interiors(point=(curr_pos[0], curr_pos[1]),
+                                                                  geoset=self.grb_obj.solid_geometry)
+
+                clicked_poly = self.get_selected_interior(clicked_poly, point=(curr_pos[0], curr_pos[1]))
+
+            else:
+                clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]), geoset=self.grb_obj.solid_geometry)
 
             if self.app.selection_type is not None:
                 self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
@@ -2380,7 +2422,7 @@ class ToolIsolation(AppTool, Gerber):
 
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_poly_mouse_click_release)
-                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_pres)
+                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.kp)
@@ -2395,7 +2437,11 @@ class ToolIsolation(AppTool, Gerber):
 
             if self.poly_dict:
                 poly_list = deepcopy(list(self.poly_dict.values()))
-                self.isolate(isolated_obj=self.grb_obj, geometry=poly_list)
+                if self.poly_int_cb.get_value() is True:
+                    # isolate the interior polygons with a negative tool
+                    self.isolate(isolated_obj=self.grb_obj, geometry=poly_list, negative_dia=True)
+                else:
+                    self.isolate(isolated_obj=self.grb_obj, geometry=poly_list)
                 self.poly_dict.clear()
             else:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
@@ -2709,7 +2755,7 @@ class ToolIsolation(AppTool, Gerber):
 
                 if self.app.is_legacy is False:
                     self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_poly_mouse_click_release)
-                    self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_pres)
+                    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.kp)
@@ -2722,6 +2768,127 @@ class ToolIsolation(AppTool, Gerber):
             self.delete_moving_selection_shape()
             self.delete_tool_selection_shape()
 
+    def on_iso_tool_add_from_db_executed(self, tool):
+        """
+        Here add the tool from DB  in the selected geometry object
+        :return:
+        """
+        tool_from_db = deepcopy(tool)
+
+        res = self.on_tool_from_db_inserted(tool=tool_from_db)
+
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                wdg = self.app.ui.plot_tab_area.widget(idx)
+                wdg.deleteLater()
+                self.app.ui.plot_tab_area.removeTab(idx)
+
+        if res == 'fail':
+            return
+        self.app.inform.emit('[success] %s' % _("Tool from DB added in Tool Table."))
+
+        # select last tool added
+        toolid = res
+        for row in range(self.tools_table.rowCount()):
+            if int(self.tools_table.item(row, 3).text()) == toolid:
+                self.tools_table.selectRow(row)
+        self.on_row_selection_change()
+
+    def on_tool_from_db_inserted(self, tool):
+        """
+        Called from the Tools DB object through a App method when adding a tool from Tools Database
+        :param tool: a dict with the tool data
+        :return: None
+        """
+
+        self.ui_disconnect()
+        self.units = self.app.defaults['units'].upper()
+
+        tooldia = float(tool['tooldia'])
+
+        # construct a list of all 'tooluid' in the self.tools
+        tool_uid_list = []
+        for tooluid_key in self.iso_tools:
+            tool_uid_item = int(tooluid_key)
+            tool_uid_list.append(tool_uid_item)
+
+        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
+        if not tool_uid_list:
+            max_uid = 0
+        else:
+            max_uid = max(tool_uid_list)
+        tooluid = max_uid + 1
+
+        tooldia = float('%.*f' % (self.decimals, tooldia))
+
+        tool_dias = []
+        for k, v in self.iso_tools.items():
+            for tool_v in v.keys():
+                if tool_v == 'tooldia':
+                    tool_dias.append(float('%.*f' % (self.decimals, (v[tool_v]))))
+
+        if float('%.*f' % (self.decimals, tooldia)) in tool_dias:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Tool already in Tool Table."))
+            self.ui_connect()
+            return 'fail'
+
+        self.iso_tools.update({
+            tooluid: {
+                'tooldia': float('%.*f' % (self.decimals, tooldia)),
+                'offset': tool['offset'],
+                'offset_value': tool['offset_value'],
+                'type': tool['type'],
+                'tool_type': tool['tool_type'],
+                'data': deepcopy(tool['data']),
+                'solid_geometry': []
+            }
+        })
+
+        self.iso_tools[tooluid]['data']['name'] = '_iso'
+
+        self.app.inform.emit('[success] %s' % _("New tool added to Tool Table."))
+
+        self.ui_connect()
+        self.build_ui()
+
+        # if self.tools_table.rowCount() != 0:
+        #     self.param_frame.setDisabled(False)
+
+    def on_tool_add_from_db_clicked(self):
+        """
+        Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
+        and display the Tools Database tab in the form needed for the Tool adding
+        :return: None
+        """
+
+        # if the Tools Database is already opened focus on it
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.app.ui.plot_tab_area.setCurrentWidget(self.app.tools_db_tab)
+                break
+        self.app.on_tools_database(source='iso')
+        self.app.tools_db_tab.ok_to_add = True
+        self.app.tools_db_tab.buttons_frame.hide()
+        self.app.tools_db_tab.add_tool_from_db.show()
+        self.app.tools_db_tab.cancel_tool_from_db.show()
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+    def reset_usage(self):
+        self.obj_name = ""
+        self.grb_obj = None
+
+        self.first_click = False
+        self.cursor_pos = None
+        self.mouse_is_dragging = False
+
+        prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False
+        if prog_plot:
+            self.temp_shapes.clear(update=True)
+
+        self.sel_rect = []
+
     @staticmethod
     def poly2rings(poly):
         return [poly.exterior] + [interior for interior in poly.interiors]
@@ -2797,7 +2964,7 @@ class ToolIsolation(AppTool, Gerber):
         return geom
 
     @staticmethod
-    def generate_rest_geometry(geometry, tooldia, passes, overlap, invert, env_iso_type=2,
+    def generate_rest_geometry(geometry, tooldia, passes, overlap, invert, env_iso_type=2, negative_dia=None,
                                prog_plot="normal", prog_plot_handler=None):
         """
         Will try to isolate the geometry and return a tuple made of list of paths made through isolation
@@ -2815,6 +2982,8 @@ class ToolIsolation(AppTool, Gerber):
         :type invert:               bool
         :param env_iso_type:        can be either 0 = keep exteriors or 1 = keep interiors or 2 = keep all paths
         :type env_iso_type:         int
+        :param negative_dia:    isolate the geometry with a negative value for the tool diameter
+        :type negative_dia:     bool
         :param prog_plot:           kind of plotting: "progressive" or "normal"
         :type prog_plot:            str
         :param prog_plot_handler:   method used to plot shapes if plot_prog is "proggressive"
@@ -2834,6 +3003,9 @@ class ToolIsolation(AppTool, Gerber):
 
             for nr_pass in range(passes):
                 iso_offset = tooldia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * tooldia)
+                if negative_dia:
+                    iso_offset = -iso_offset
+
                 buf_chek = iso_offset * 2
                 check_geo = geo.buffer(buf_chek)
 
@@ -2925,123 +3097,47 @@ class ToolIsolation(AppTool, Gerber):
 
         return isolated_geo, not_isolated_geo
 
-    def on_iso_tool_add_from_db_executed(self, tool):
-        """
-        Here add the tool from DB  in the selected geometry object
-        :return:
-        """
-        tool_from_db = deepcopy(tool)
-
-        res = self.on_tool_from_db_inserted(tool=tool_from_db)
-
-        for idx in range(self.app.ui.plot_tab_area.count()):
-            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
-                wdg = self.app.ui.plot_tab_area.widget(idx)
-                wdg.deleteLater()
-                self.app.ui.plot_tab_area.removeTab(idx)
+    @staticmethod
+    def get_selected_interior(poly: Polygon, point: tuple) -> [Polygon, None]:
+        try:
+            ints = [Polygon(x) for x in poly.interiors]
+        except AttributeError:
+            return None
 
-        if res == 'fail':
-            return
-        self.app.inform.emit('[success] %s' % _("Tool from DB added in Tool Table."))
+        for poly in ints:
+            if poly.contains(Point(point)):
+                return poly
 
-        # select last tool added
-        toolid = res
-        for row in range(self.tools_table.rowCount()):
-            if int(self.tools_table.item(row, 3).text()) == toolid:
-                self.tools_table.selectRow(row)
-        self.on_row_selection_change()
+        return None
 
-    def on_tool_from_db_inserted(self, tool):
-        """
-        Called from the Tools DB object through a App method when adding a tool from Tools Database
-        :param tool: a dict with the tool data
-        :return: None
+    def find_polygon_ignore_interiors(self, point, geoset=None):
         """
+        Find an object that object.contains(Point(point)) in
+        poly, which can can be iterable, contain iterable of, or
+        be itself an implementer of .contains(). Will test the Polygon as it is full with no interiors.
 
-        self.ui_disconnect()
-        self.units = self.app.defaults['units'].upper()
-
-        tooldia = float(tool['tooldia'])
-
-        # construct a list of all 'tooluid' in the self.tools
-        tool_uid_list = []
-        for tooluid_key in self.iso_tools:
-            tool_uid_item = int(tooluid_key)
-            tool_uid_list.append(tool_uid_item)
-
-        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
-        if not tool_uid_list:
-            max_uid = 0
-        else:
-            max_uid = max(tool_uid_list)
-        tooluid = max_uid + 1
-
-        tooldia = float('%.*f' % (self.decimals, tooldia))
-
-        tool_dias = []
-        for k, v in self.iso_tools.items():
-            for tool_v in v.keys():
-                if tool_v == 'tooldia':
-                    tool_dias.append(float('%.*f' % (self.decimals, (v[tool_v]))))
-
-        if float('%.*f' % (self.decimals, tooldia)) in tool_dias:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Tool already in Tool Table."))
-            self.ui_connect()
-            return 'fail'
-
-        self.iso_tools.update({
-            tooluid: {
-                'tooldia': float('%.*f' % (self.decimals, tooldia)),
-                'offset': tool['offset'],
-                'offset_value': tool['offset_value'],
-                'type': tool['type'],
-                'tool_type': tool['tool_type'],
-                'data': deepcopy(tool['data']),
-                'solid_geometry': []
-            }
-        })
-
-        self.iso_tools[tooluid]['data']['name'] = '_iso'
-
-        self.app.inform.emit('[success] %s' % _("New tool added to Tool Table."))
-
-        self.ui_connect()
-        self.build_ui()
-
-        # if self.tools_table.rowCount() != 0:
-        #     self.param_frame.setDisabled(False)
-
-    def on_tool_add_from_db_clicked(self):
+        :param point: See description
+        :param geoset: a polygon or list of polygons where to find if the param point is contained
+        :return: Polygon containing point or None.
         """
-        Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
-        and display the Tools Database tab in the form needed for the Tool adding
-        :return: None
-        """
-
-        # if the Tools Database is already opened focus on it
-        for idx in range(self.app.ui.plot_tab_area.count()):
-            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
-                self.app.ui.plot_tab_area.setCurrentWidget(self.app.tools_db_tab)
-                break
-        self.app.on_tools_database(source='iso')
-        self.app.tools_db_tab.ok_to_add = True
-        self.app.tools_db_tab.buttons_frame.hide()
-        self.app.tools_db_tab.add_tool_from_db.show()
-        self.app.tools_db_tab.cancel_tool_from_db.show()
 
-    def reset_fields(self):
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-
-    def reset_usage(self):
-        self.obj_name = ""
-        self.grb_obj = None
-
-        self.first_click = False
-        self.cursor_pos = None
-        self.mouse_is_dragging = False
-
-        prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False
-        if prog_plot:
-            self.temp_shapes.clear(update=True)
-
-        self.sel_rect = []
+        if geoset is None:
+            geoset = self.solid_geometry
+
+        try:  # Iterable
+            for sub_geo in geoset:
+                p = self.find_polygon_ignore_interiors(point, geoset=sub_geo)
+                if p is not None:
+                    return p
+        except TypeError:  # Non-iterable
+            try:  # Implements .contains()
+                if isinstance(geoset, LinearRing):
+                    geoset = Polygon(geoset)
+
+                poly_ext = Polygon(geoset.exterior)
+                if poly_ext.contains(Point(point)):
+                    return geoset
+            except AttributeError:  # Does not implement .contains()
+                return None
+
+        return None

+ 1 - 0
defaults.py

@@ -402,6 +402,7 @@ class FlatCAMDefaults:
         "tools_iso_combine_passes": False,
         "tools_iso_isoexcept":      False,
         "tools_iso_selection":      _("All"),
+        "tools_iso_poly_ints":      False,
         "tools_iso_area_shape":     "square",
         "tools_iso_plotting":       'normal',