Przeglądaj źródła

- working on Isolation Tool: made to work the Isolation with multiple tools without rest machining

Marius Stanciu 5 lat temu
rodzic
commit
90eb581a34

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

@@ -281,7 +281,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         )
         self.select_combo = FCComboBox()
         self.select_combo.addItems(
-            [_("All"), _("Area Selection"), _("Reference Object")]
+            [_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
         )
         self.select_combo.setObjectName("i_selection")
 

+ 23 - 0
AppObjects/FlatCAMGerber.py

@@ -491,6 +491,29 @@ class GerberObject(FlatCAMObj, Gerber):
                 return 'fail'
         return geom
 
+    def follow_geo(self, outname=None):
+        """
+        Creates a geometry object "following" the gerber paths.
+
+        :return: None
+        """
+
+        if outname is None:
+            follow_name = self.options["name"] + "_follow"
+        else:
+            follow_name = outname
+
+        def follow_init(follow_obj, app):
+            # Propagate options
+            follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
+            follow_obj.solid_geometry = self.follow_geometry
+
+        # TODO: Do something if this is None. Offer changing name?
+        try:
+            self.app.app_obj.new_object("geometry", follow_name, follow_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)
+
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
             return

+ 422 - 307
AppTools/ToolIsolation.py

@@ -507,7 +507,7 @@ class ToolIsolation(AppTool, Gerber):
         )
         self.select_combo = FCComboBox()
         self.select_combo.addItems(
-            [_("All"), _("Area Selection"), _("Reference Object")]
+            [_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
         )
         self.select_combo.setObjectName("i_selection")
 
@@ -666,6 +666,11 @@ class ToolIsolation(AppTool, Gerber):
 
         self.kp = None
 
+        # store geometry from Polygon selection
+        self.poly_dict = {}
+
+        self.grid_status_memory = self.app.ui.grid_snap_btn.isChecked()
+
         # store here solid_geometry when there are tool with isolation job
         self.solid_geometry = []
 
@@ -717,7 +722,7 @@ class ToolIsolation(AppTool, Gerber):
         self.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
         self.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
 
-        self.generate_iso_button.clicked.connect(self.on_isolate_click)
+        self.generate_iso_button.clicked.connect(self.on_iso_button_click)
         self.reset_button.clicked.connect(self.set_tool_ui)
 
         # Cleanup on Graceful exit (CTRL+ALT+X combo key)
@@ -989,7 +994,7 @@ class ToolIsolation(AppTool, Gerber):
         self.iso_overlap_entry.set_value(self.app.defaults["tools_iso_overlap"])
         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_combine_passes"])
+        self.area_shape_radio.set_value(self.app.defaults["tools_iso_area_shape"])
 
         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"])
@@ -999,10 +1004,12 @@ class ToolIsolation(AppTool, Gerber):
 
         self.on_tool_type(val=self.tool_type_radio.get_value())
 
+        outname = self.app.collection.get_by_name(self.object_combo.get_value()).options['name']
+
         # init the working variables
         self.default_data.clear()
         self.default_data = {
-            "name": '_ncc',
+            "name": outname + '_iso',
             "plot": self.app.defaults["geometry_plot"],
             "cutz": float(self.cutz_entry.get_value()),
             "vtipdia": float(self.tipdia_entry.get_value()),
@@ -1300,9 +1307,16 @@ class ToolIsolation(AppTool, Gerber):
             self.area_shape_label.show()
             self.area_shape_radio.show()
 
-            # disable rest-machining for area painting
+            # disable rest-machining for area isolation
             self.rest_cb.set_value(False)
             self.rest_cb.setDisabled(True)
+        elif val == _("Polygon Selection"):
+            self.reference_combo.hide()
+            self.reference_combo_label.hide()
+            self.reference_combo_type.hide()
+            self.reference_combo_type_label.hide()
+            self.area_shape_label.hide()
+            self.area_shape_radio.hide()
         else:
             self.reference_combo.show()
             self.reference_combo_label.show()
@@ -1691,60 +1705,89 @@ class ToolIsolation(AppTool, Gerber):
 
     def on_iso_button_click(self, *args):
 
-        obj = self.app.collection.get_active()
+        self.obj_name = self.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.grb_obj = self.app.collection.get_by_name(self.obj_name)
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
+            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
 
-        self.iso_type = 2
-        if self.ui.iso_type_radio.get_value() == 'ext':
-            self.iso_type = 0
-        if self.ui.iso_type_radio.get_value() == 'int':
-            self.iso_type = 1
+        if self.grb_obj is None:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
+            return
 
         def worker_task(iso_obj, app_obj):
             with self.app.proc_container.new(_("Isolating...")):
-                if self.ui.follow_cb.get_value() is True:
-                    iso_obj.follow_geo()
-                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
-                    iso_obj.ui.plot_cb.toggle()
-                else:
-                    app_obj.defaults.report_usage("gerber_on_iso_button")
-                    self.read_form()
-
-                    iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single'
-                    self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
+                self.isolate_handler(iso_obj)
 
-        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
+        self.app.worker_task.emit({'fcn': worker_task, 'params': [self.grb_obj, self.app]})
 
-    def follow_geo(self, outname=None):
+    def follow_geo(self, followed_obj, outname):
         """
         Creates a geometry object "following" the gerber paths.
 
+        :param followed_obj:    Gerber object for which to generate the follow geometry
+        :type followed_obj:     AppObjects.FlatCAMGerber.GerberObject
+        :param outname:         Nme of the resulting Geometry object
+        :type outname:          str
         :return: None
         """
 
-        # default_name = self.options["name"] + "_follow"
-        # follow_name = outname or default_name
-
-        if outname is None:
-            follow_name = self.options["name"] + "_follow"
-        else:
-            follow_name = outname
-
         def follow_init(follow_obj, app):
             # Propagate options
-            follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-            follow_obj.solid_geometry = self.follow_geometry
+            follow_obj.options["cnctooldia"] = str(tooldia)
+            follow_obj.solid_geometry = self.grb_obj.follow_geometry
+
+        # in the end toggle the visibility of the origin object so we can see the generated Geometry
+        followed_obj.ui.plot_cb.set_value(False)
+        follow_name = outname
+
+        for tool in self.iso_tools:
+            tooldia = self.iso_tools[tool]['tooldia']
+            new_name = "%s_%.*f" % (follow_name, self.decimals, tooldia)
+
+            follow_state = self.iso_tools[tool]['data']['tools_iso_follow']
+            if follow_state:
+                ret = self.app.app_obj.new_object("geometry", new_name, follow_init)
+                if ret == 'fail':
+                    self.app.inform.emit("[ERROR_NOTCL] %s: %.*f" % (
+                        _("Failed to create Follow Geometry with tool diameter"), self.decimals, tooldia))
+                else:
+                    self.app.inform.emit("[success] %s: %.*f" % (
+                        _("Follow Geometry was created with tool diameter"), self.decimals, tooldia))
 
-        # TODO: Do something if this is None. Offer changing name?
-        try:
-            self.app.app_obj.new_object("geometry", follow_name, follow_init)
-        except Exception as e:
-            return "Operation failed: %s" % str(e)
+    def isolate_handler(self, isolated_obj):
+        """
+        Creates a geometry object with paths around the gerber features.
 
-    def isolate_handler(self, iso_type, iso_scope):
+        :param isolated_obj:    Gerber object for which to generate the isolating routing geometry
+        :type isolated_obj:     AppObjects.FlatCAMGerber.GerberObject
+        :return: None
+        """
+        selection = self.select_combo.get_value()
 
-        if iso_scope == 'all':
-            self.isolate(iso_type=iso_type)
-        else:
+        if selection == _("All"):
+            full_geo = isolated_obj.solid_geometry
+            self.isolate(isolated_obj=isolated_obj, geometry=full_geo)
+        elif selection == _("Area Selection"):
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
+
+            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)
+
+        elif selection == _("Polygon Selection"):
             # disengage the grid snapping since it may be hard to click on polygons with grid snapping on
             if self.app.ui.grid_snap_btn.isChecked():
                 self.grid_status_memory = True
@@ -1753,146 +1796,149 @@ class ToolIsolation(AppTool, Gerber):
                 self.grid_status_memory = False
 
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+            self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
 
             if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_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.mr)
 
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
+        elif selection == _("Reference Object"):
+            ref_obj = self.app.collection.get_by_name(self.reference_combo.get_value())
+            ref_geo = cascaded_union(ref_obj.solid_geometry)
+            use_geo = cascaded_union(isolated_obj.solid_geometry).difference(ref_geo)
+            self.isolate(isolated_obj=isolated_obj, geometry=use_geo)
 
-    def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
-                milling_type=None, follow=None, plot=True):
+    def isolate(self, isolated_obj, geometry=None, limited_area=None, plot=True):
         """
         Creates an isolation routing geometry object in the project.
 
-        :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
-        :param geometry: specific geometry to isolate
-        :param dia: Tool diameter
-        :param passes: Number of tool widths to cut
-        :param overlap: Overlap between passes in fraction of tool diameter
-        :param outname: Base name of the output object
-        :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
-        :param milling_type: type of milling: conventional or climbing
-        :param follow: Boolean: if to generate a 'follow' geometry
-        :param plot: Boolean: if to plot the resulting geometry object
+        :param isolated_obj:    Gerber object for which to generate the isolating routing geometry
+        :type isolated_obj:     AppObjects.FlatCAMGerber.GerberObject
+        :param geometry:        specific geometry to isolate
+        :type geometry:         List of Shapely polygon
+        :param limited_area:    if not None clear only this area
+        :type limited_area:     Shapely Polygon or a list of them
+        :param plot:            if to plot the resulting geometry object
+        :type plot:             bool
         :return: None
         """
 
-        if geometry is None:
-            work_geo = self.follow_geometry if follow is True else self.solid_geometry
-        else:
-            work_geo = geometry
-
-        if dia is None:
-            dia = float(self.options["isotooldia"])
+        iso_name = isolated_obj.options["name"]
+        combine = self.combine_passes_cb.get_value()
+        tools_storage = self.iso_tools
 
-        if passes is None:
-            passes = int(self.options["isopasses"])
+        if combine:
+            total_solid_geometry = []
 
-        if overlap is None:
-            overlap = float(self.options["isooverlap"])
+            for tool in tools_storage:
+                tool_dia = tools_storage[tool]['tooldia']
+                tool_type = tools_storage[tool]['tool_type']
+                tool_data = tools_storage[tool]['data']
 
-        overlap /= 100.0
+                to_follow = tool_data['tools_iso_follow']
 
-        combine = self.options["combine_passes"] if combine is None else bool(combine)
+                work_geo = geometry
+                if work_geo is None:
+                    work_geo = isolated_obj.follow_geometry if to_follow else isolated_obj.solid_geometry
 
-        if milling_type is None:
-            milling_type = self.options["milling_type"]
+                iso_t = {
+                    'ext': 0,
+                    'int': 1,
+                    'full': 2
+                }[tool_data['tools_iso_isotype']]
 
-        if iso_type is None:
-            iso_t = 2
-        else:
-            iso_t = iso_type
+                passes = tool_data['tools_iso_passes']
+                overlap = tool_data['tools_iso_overlap']
+                overlap /= 100.0
 
-        base_name = self.options["name"]
+                milling_type = tool_data['tools_iso_milling_type']
 
-        if combine:
-            if outname is None:
-                if self.iso_type == 0:
-                    iso_name = base_name + "_ext_iso"
-                elif self.iso_type == 1:
-                    iso_name = base_name + "_int_iso"
-                else:
-                    iso_name = base_name + "_iso"
-            else:
-                iso_name = outname
+                iso_except = tool_data['tools_iso_isoexcept']
 
-            def iso_init(geo_obj, app_obj):
-                # Propagate options
-                geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper()
+                outname = "%s_%.*f" % (isolated_obj.options["name"], self.decimals, float(tool_dia))
 
-                geo_obj.solid_geometry = []
+                iso_name = outname + "_iso"
+                if iso_t == 0:
+                    iso_name = outname + "_ext_iso"
+                elif iso_t == 1:
+                    iso_name = outname + "_int_iso"
 
                 # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                if self.ui.tool_type_radio.get_value() == 'v':
+                if tool_type.lower() == 'v':
                     new_cutz = self.ui.cutz_spinner.get_value()
                     new_vtipdia = self.ui.tipdia_spinner.get_value()
                     new_vtipangle = self.ui.tipangle_spinner.get_value()
                     tool_type = 'V'
+                    tool_data.update({
+                        "name": iso_name,
+                        "cutz": new_cutz,
+                        "vtipdia": new_vtipdia,
+                        "vtipangle": new_vtipangle,
+                    })
                 else:
-                    new_cutz = self.app.defaults['geometry_cutz']
-                    new_vtipdia = self.app.defaults['geometry_vtipdia']
-                    new_vtipangle = self.app.defaults['geometry_vtipangle']
+                    tool_data.update({
+                        "name": iso_name,
+                    })
                     tool_type = 'C1'
 
-                # store here the default data for Geometry Data
-                default_data = {}
-                default_data.update({
-                    "name": iso_name,
-                    "plot": self.app.defaults['geometry_plot'],
-                    "cutz": new_cutz,
-                    "vtipdia": new_vtipdia,
-                    "vtipangle": new_vtipangle,
-                    "travelz": self.app.defaults['geometry_travelz'],
-                    "feedrate": self.app.defaults['geometry_feedrate'],
-                    "feedrate_z": self.app.defaults['geometry_feedrate_z'],
-                    "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
-                    "dwell": self.app.defaults['geometry_dwell'],
-                    "dwelltime": self.app.defaults['geometry_dwelltime'],
-                    "multidepth": self.app.defaults['geometry_multidepth'],
-                    "ppname_g": self.app.defaults['geometry_ppname_g'],
-                    "depthperpass": self.app.defaults['geometry_depthperpass'],
-                    "extracut": self.app.defaults['geometry_extracut'],
-                    "extracut_length": self.app.defaults['geometry_extracut_length'],
-                    "toolchange": self.app.defaults['geometry_toolchange'],
-                    "toolchangez": self.app.defaults['geometry_toolchangez'],
-                    "endz": self.app.defaults['geometry_endz'],
-                    "spindlespeed": self.app.defaults['geometry_spindlespeed'],
-                    "toolchangexy": self.app.defaults['geometry_toolchangexy'],
-                    "startz": self.app.defaults['geometry_startz']
-                })
+                solid_geo = []
+                for nr_pass in range(passes):
+                    iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * tool_dia)
+
+                    # if milling type is climb then the move is counter-clockwise around features
+                    mill_dir = 1 if milling_type == 'cl' else 0
 
-                geo_obj.tools = {}
-                geo_obj.tools['1'] = {}
-                geo_obj.tools.update({
-                    '1': {
-                        'tooldia': float(self.options["isotooldia"]),
+                    iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
+                                                     follow=to_follow, nr_passes=nr_pass)
+                    if iso_geo == 'fail':
+                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
+                        continue
+                    try:
+                        for geo in iso_geo:
+                            solid_geo.append(geo)
+                    except TypeError:
+                        solid_geo.append(iso_geo)
+
+                    # ############################################################
+                    # ########## AREA SUBTRACTION ################################
+                    # ############################################################
+                    if iso_except:
+                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
+                        solid_geo = self.area_subtraction(solid_geo)
+
+                    if limited_area:
+                        self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo"))
+                        solid_geo = self.area_intersection(solid_geo, intersection_geo=limited_area)
+
+                tools_storage.update({
+                    tool: {
+                        'tooldia': float(tool_dia),
                         'offset': 'Path',
                         'offset_value': 0.0,
                         'type': _('Rough'),
                         'tool_type': tool_type,
-                        'data': default_data,
-                        'solid_geometry': geo_obj.solid_geometry
+                        'data': tool_data,
+                        'solid_geometry': deepcopy(solid_geo)
                     }
                 })
 
-                for nr_pass in range(passes):
-                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
+                total_solid_geometry += solid_geo
 
-                    # if milling type is climb then the move is counter-clockwise around features
-                    mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow, nr_passes=nr_pass)
+            def iso_init(geo_obj, app_obj):
+                geo_obj.options["cnctooldia"] = str(tool_dia)
 
-                    if geom == 'fail':
-                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
-                    geo_obj.solid_geometry.append(geom)
+                geo_obj.tools = dict(tools_storage)
+                geo_obj.solid_geometry = total_solid_geometry
+                # even if combine is checked, one pass is still single-geo
 
-                    # update the geometry in the tools
-                    geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry
+                if len(self.iso_tools) > 1:
+                    geo_obj.multigeo = True
+                else:
+                    passes = float(self.iso_tools[0]['data']['tools_iso_passes'])
+                    geo_obj.multigeo = True if passes > 1 else False
 
                 # detect if solid_geometry is empty and this require list flattening which is "heavy"
                 # or just looking in the lists (they are one level depth) and if any is not empty
@@ -1910,158 +1956,154 @@ class ToolIsolation(AppTool, Gerber):
                         empty_cnt += 1
 
                 if empty_cnt == len(geo_obj.solid_geometry):
-                    raise ValidationError("Empty Geometry", None)
+                    app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Empty Geometry in"), geo_obj.options["name"]))
+                    return 'fail'
                 else:
-                    app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
-
-                # even if combine is checked, one pass is still single-geo
-                geo_obj.multigeo = True if passes > 1 else False
-
-                # ############################################################
-                # ########## AREA SUBTRACTION ################################
-                # ############################################################
-                if self.ui.except_cb.get_value():
-                    self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                    geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
+                    app_obj.inform.emit('[success] %s: %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
 
-            # TODO: Do something if this is None. Offer changing name?
             self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
+
         else:
-            for i in range(passes):
-                offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia)
-                if passes > 1:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso" + str(i + 1)
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso" + str(i + 1)
-                        else:
-                            iso_name = base_name + "_iso" + str(i + 1)
-                    else:
-                        iso_name = outname
-                else:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso"
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso"
-                        else:
-                            iso_name = base_name + "_iso"
-                    else:
-                        iso_name = outname
+            for tool in tools_storage:
+                tool_data = tools_storage[tool]['data']
+                to_follow = tool_data['tools_iso_follow']
+
+                work_geo = geometry
+                if work_geo is None:
+                    work_geo = isolated_obj.follow_geometry if to_follow else isolated_obj.solid_geometry
+
+                iso_t = {
+                    'ext': 0,
+                    'int': 1,
+                    'full': 2
+                }[tool_data['tools_iso_isotype']]
+
+                passes = tool_data['tools_iso_passes']
+                overlap = tool_data['tools_iso_overlap']
+                overlap /= 100.0
+
+                milling_type = tool_data['tools_iso_milling_type']
+
+                iso_except = tool_data['tools_iso_isoexcept']
 
-                def iso_init(geo_obj, fc_obj):
-                    # Propagate options
-                    geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                    if self.ui.tool_type_radio.get_value() == 'v':
-                        geo_obj.tool_type = 'V'
+                for i in range(passes):
+                    tool_dia = tools_storage[tool]['tooldia']
+                    tool_type = tools_storage[tool]['tool_type']
+
+                    iso_offset = tool_dia * ((2 * i + 1) / 2.0000001) - (i * overlap * tool_dia)
+                    outname = "%s_%.*f" % (isolated_obj.options["name"], self.decimals, float(tool_dia))
+
+                    if passes > 1:
+                        iso_name = outname + "_iso" + str(i + 1)
+                        if iso_t == 0:
+                            iso_name = outname + "_ext_iso" + str(i + 1)
+                        elif iso_t == 1:
+                            iso_name = outname + "_int_iso" + str(i + 1)
                     else:
-                        geo_obj.tool_type = 'C1'
+                        iso_name = outname + "_iso"
+                        if iso_t == 0:
+                            iso_name = outname + "_ext_iso"
+                        elif iso_t == 1:
+                            iso_name = outname + "_int_iso"
 
                     # if milling type is climb then the move is counter-clockwise around features
                     mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow,
-                                                  nr_passes=i)
 
-                    if geom == 'fail':
-                        fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
+                    iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
+                                                     follow=to_follow, nr_passes=i)
+                    if iso_geo == 'fail':
+                        self.app.inform.emit(
+                            '[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
+                        continue
+
+                    # ############################################################
+                    # ########## AREA SUBTRACTION ################################
+                    # ############################################################
+                    if iso_except:
+                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
+                        iso_geo = self.area_subtraction(iso_geo)
 
-                    geo_obj.solid_geometry = geom
+                    if limited_area:
+                        self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo"))
+                        iso_geo = self.area_intersection(iso_geo, intersection_geo=limited_area)
 
-                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                    # even if the resulting geometry is not multigeo we add the tools dict which will hold the data
-                    # required to be transfered to the Geometry object
-                    if self.ui.tool_type_radio.get_value() == 'v':
+                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in
+                    # Gerber UI
+                    if tool_type.lower() == 'v':
                         new_cutz = self.ui.cutz_spinner.get_value()
                         new_vtipdia = self.ui.tipdia_spinner.get_value()
                         new_vtipangle = self.ui.tipangle_spinner.get_value()
                         tool_type = 'V'
+                        tool_data.update({
+                            "name": iso_name,
+                            "cutz": new_cutz,
+                            "vtipdia": new_vtipdia,
+                            "vtipangle": new_vtipangle,
+                        })
                     else:
-                        new_cutz = self.app.defaults['geometry_cutz']
-                        new_vtipdia = self.app.defaults['geometry_vtipdia']
-                        new_vtipangle = self.app.defaults['geometry_vtipangle']
+                        tool_data.update({
+                            "name": iso_name,
+                        })
                         tool_type = 'C1'
 
-                    # store here the default data for Geometry Data
-                    default_data = {}
-                    default_data.update({
-                        "name": iso_name,
-                        "plot": self.app.defaults['geometry_plot'],
-                        "cutz": new_cutz,
-                        "vtipdia": new_vtipdia,
-                        "vtipangle": new_vtipangle,
-                        "travelz": self.app.defaults['geometry_travelz'],
-                        "feedrate": self.app.defaults['geometry_feedrate'],
-                        "feedrate_z": self.app.defaults['geometry_feedrate_z'],
-                        "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
-                        "dwell": self.app.defaults['geometry_dwell'],
-                        "dwelltime": self.app.defaults['geometry_dwelltime'],
-                        "multidepth": self.app.defaults['geometry_multidepth'],
-                        "ppname_g": self.app.defaults['geometry_ppname_g'],
-                        "depthperpass": self.app.defaults['geometry_depthperpass'],
-                        "extracut": self.app.defaults['geometry_extracut'],
-                        "extracut_length": self.app.defaults['geometry_extracut_length'],
-                        "toolchange": self.app.defaults['geometry_toolchange'],
-                        "toolchangez": self.app.defaults['geometry_toolchangez'],
-                        "endz": self.app.defaults['geometry_endz'],
-                        "spindlespeed": self.app.defaults['geometry_spindlespeed'],
-                        "toolchangexy": self.app.defaults['geometry_toolchangexy'],
-                        "startz": self.app.defaults['geometry_startz']
-                    })
-
-                    geo_obj.tools = {}
-                    geo_obj.tools['1'] = {}
-                    geo_obj.tools.update({
-                        '1': {
-                            'tooldia': float(self.options["isotooldia"]),
-                            'offset': 'Path',
-                            'offset_value': 0.0,
-                            'type': _('Rough'),
-                            'tool_type': tool_type,
-                            'data': default_data,
-                            'solid_geometry': geo_obj.solid_geometry
-                        }
-                    })
+                    def iso_init(geo_obj, fc_obj):
+                        # Propagate options
+                        geo_obj.options["cnctooldia"] = str(tool_dia)
+                        geo_obj.solid_geometry = deepcopy(iso_geo)
+
+                        # ############################################################
+                        # ########## AREA SUBTRACTION ################################
+                        # ############################################################
+                        if self.except_cb.get_value():
+                            self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
+                            geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
+
+                        geo_obj.tools = {}
+                        geo_obj.tools['1'] = {}
+                        geo_obj.tools.update({
+                            '1': {
+                                'tooldia': float(tool_dia),
+                                'offset': 'Path',
+                                'offset_value': 0.0,
+                                'type': _('Rough'),
+                                'tool_type': tool_type,
+                                'data': tool_data,
+                                'solid_geometry': geo_obj.solid_geometry
+                            }
+                        })
+
+                        # detect if solid_geometry is empty and this require list flattening which is "heavy"
+                        # or just looking in the lists (they are one level depth) and if any is not empty
+                        # proceed with object creation, if there are empty and the number of them is the length
+                        # of the list then we have an empty solid_geometry which should raise a Custom Exception
+                        empty_cnt = 0
+                        if not isinstance(geo_obj.solid_geometry, list):
+                            geo_obj.solid_geometry = [geo_obj.solid_geometry]
+
+                        for g in geo_obj.solid_geometry:
+                            if g:
+                                break
+                            else:
+                                empty_cnt += 1
 
-                    # detect if solid_geometry is empty and this require list flattening which is "heavy"
-                    # or just looking in the lists (they are one level depth) and if any is not empty
-                    # proceed with object creation, if there are empty and the number of them is the length
-                    # of the list then we have an empty solid_geometry which should raise a Custom Exception
-                    empty_cnt = 0
-                    if not isinstance(geo_obj.solid_geometry, list):
-                        geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                    for g in geo_obj.solid_geometry:
-                        if g:
-                            break
+                        if empty_cnt == len(geo_obj.solid_geometry):
+                            fc_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (
+                                _("Empty Geometry in"), geo_obj.options["name"]))
+                            return 'fail'
                         else:
-                            empty_cnt += 1
-
-                    if empty_cnt == len(geo_obj.solid_geometry):
-                        raise ValidationError("Empty Geometry", None)
-                    else:
-                        fc_obj.inform.emit('[success] %s: %s' %
-                                            (_("Isolation geometry created"), geo_obj.options["name"]))
-                    geo_obj.multigeo = False
-
-                    # ############################################################
-                    # ########## AREA SUBTRACTION ################################
-                    # ############################################################
-                    if self.ui.except_cb.get_value():
-                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                        geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
+                            fc_obj.inform.emit('[success] %s: %s' %
+                                                (_("Isolation geometry created"), geo_obj.options["name"]))
+                        geo_obj.multigeo = False
 
-                # TODO: Do something if this is None. Offer changing name?
-                self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
+                    # TODO: Do something if this is None. Offer changing name?
+                    self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
 
-    def area_subtraction(self, geo, subtractor_geo=None):
+    def area_subtraction(self, geo, subtractor_geo):
         """
         Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo
 
-        :param geo: target geometry from which to subtract
-        :param subtractor_geo: geometry that acts as subtractor
+        :param geo:             target geometry from which to subtract
+        :param subtractor_geo:  geometry that acts as subtractor
         :return:
         """
         new_geometry = []
@@ -2070,7 +2112,7 @@ class ToolIsolation(AppTool, Gerber):
         if subtractor_geo:
             sub_union = cascaded_union(subtractor_geo)
         else:
-            name = self.ui.obj_combo.currentText()
+            name = self.exc_obj_combo.currentText()
             subtractor_obj = self.app.collection.get_by_name(name)
             sub_union = cascaded_union(subtractor_obj.solid_geometry)
 
@@ -2115,6 +2157,60 @@ class ToolIsolation(AppTool, Gerber):
                         new_geometry.append(new_geo)
         return new_geometry
 
+    def area_intersection(self, geo, intersection_geo=None):
+        """
+        Return the intersection geometry between geo and intersection_geo
+
+        :param geo:                 target geometry
+        :param intersection_geo:    second geometry
+        :return:
+        """
+        new_geometry = []
+        target_geo = geo
+
+        intersect_union = cascaded_union(intersection_geo)
+
+        try:
+            for geo_elem in target_geo:
+                if isinstance(geo_elem, Polygon):
+                    for ring in self.poly2rings(geo_elem):
+                        new_geo = ring.intersection(intersect_union)
+                        if new_geo and not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+                elif isinstance(geo_elem, MultiPolygon):
+                    for poly in geo_elem:
+                        for ring in self.poly2rings(poly):
+                            new_geo = ring.intersection(intersect_union)
+                            if new_geo and not new_geo.is_empty:
+                                new_geometry.append(new_geo)
+                elif isinstance(geo_elem, LineString):
+                    new_geo = geo_elem.intersection(intersect_union)
+                    if new_geo:
+                        if not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+                elif isinstance(geo_elem, MultiLineString):
+                    for line_elem in geo_elem:
+                        new_geo = line_elem.intersection(intersect_union)
+                        if new_geo and not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+        except TypeError:
+            if isinstance(target_geo, Polygon):
+                for ring in self.poly2rings(target_geo):
+                    new_geo = ring.intersection(intersect_union)
+                    if new_geo:
+                        if not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+            elif isinstance(target_geo, LineString):
+                new_geo = target_geo.intersection(intersect_union)
+                if new_geo and not new_geo.is_empty:
+                    new_geometry.append(new_geo)
+            elif isinstance(target_geo, MultiLineString):
+                for line_elem in target_geo:
+                    new_geo = line_elem.intersection(intersect_union)
+                    if new_geo and not new_geo.is_empty:
+                        new_geometry.append(new_geo)
+        return new_geometry
+
     def on_mouse_click_release(self, event):
         if self.app.is_legacy is False:
             event_pos = event.pos
@@ -2139,7 +2235,7 @@ 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]))
+            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)
@@ -2179,8 +2275,10 @@ class ToolIsolation(AppTool, Gerber):
 
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
+                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_pres)
             else:
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.kp)
 
             self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
                                                                   self.app.on_mouse_click_release_over_plot)
@@ -2189,7 +2287,7 @@ class ToolIsolation(AppTool, Gerber):
 
             if self.poly_dict:
                 poly_list = deepcopy(list(self.poly_dict.values()))
-                self.isolate(iso_type=self.iso_type, geometry=poly_list)
+                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."))
@@ -2383,12 +2481,8 @@ class ToolIsolation(AppTool, Gerber):
                 return
 
             self.sel_rect = cascaded_union(self.sel_rect)
-
-            self.clear_copper(ncc_obj=self.grb_obj,
-                              sel_obj=self.bound_obj,
-                              ncctooldia=self.ncc_dia_list,
-                              isotooldia=self.iso_dia_list,
-                              outname=self.o_name)
+            self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True)
+            self.sel_rect = []
 
     # called on mouse move
     def on_mouse_move(self, event):
@@ -2689,7 +2783,7 @@ class ToolIsolation(AppTool, Gerber):
         rest_machining_choice = self.rest_cb.get_value()
 
         # determine if to use the progressive plotting
-        prog_plot = True if self.app.defaults["tools_ncc_plotting"] == 'progressive' else False
+        prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False
         tools_storage = tools_storage if tools_storage is not None else self.iso_tools
 
         # ######################################################################################################
@@ -2906,7 +3000,7 @@ class ToolIsolation(AppTool, Gerber):
                         else:
                             log.debug("There are no geometries in the cleared polygon.")
             # clean the progressive plotted shapes if it was used
-            if self.app.defaults["tools_ncc_plotting"] == 'progressive':
+            if self.app.defaults["tools_iso_plotting"] == 'progressive':
                 self.temp_shapes.clear(update=True)
 
             # delete tools with empty geometry
@@ -3221,7 +3315,7 @@ class ToolIsolation(AppTool, Gerber):
             geo_obj.options["cnctooldia"] = str(tool)
 
             # clean the progressive plotted shapes if it was used
-            if self.app.defaults["tools_ncc_plotting"] == 'progressive':
+            if self.app.defaults["tools_iso_plotting"] == 'progressive':
                 self.temp_shapes.clear(update=True)
 
             # check to see if geo_obj.tools is empty
@@ -3290,40 +3384,61 @@ class ToolIsolation(AppTool, Gerber):
     def poly2rings(poly):
         return [poly.exterior] + [interior for interior in poly.interiors]
 
-    def generate_envelope(self, offset, invert, envelope_iso_type=2, follow=None):
-        # isolation_geometry produces an envelope that is going on the left of the geometry
-        # (the copper features). To leave the least amount of burrs on the features
-        # the tool needs to travel on the right side of the features (this is called conventional milling)
-        # the first pass is the one cutting all of the features, so it needs to be reversed
-        # the other passes overlap preceding ones and cut the left over copper. It is better for them
-        # to cut on the right side of the left over copper i.e on the left side of the features.
-        try:
-            geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
-        except Exception as e:
-            log.debug('NonCopperClear.generate_envelope() --> %s' % str(e))
-            return 'fail'
+    def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0):
+        """
+        Isolation_geometry produces an envelope that is going on the left of the geometry
+        (the copper features). To leave the least amount of burrs on the features
+        the tool needs to travel on the right side of the features (this is called conventional milling)
+        the first pass is the one cutting all of the features, so it needs to be reversed
+        the other passes overlap preceding ones and cut the left over copper. It is better for them
+        to cut on the right side of the left over copper i.e on the left side of the features.
+
+        :param offset:          Offset distance to be passed to the obj.isolation_geometry() method
+        :type offset:           float
+        :param invert:          If to invert the direction of geometry (CW to CCW or reverse)
+        :type invert:           int
+        :param geometry:        Shapely Geometry for which t ogenerate envelope
+        :type geometry:
+        :param env_iso_type:    type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
+        :type env_iso_type:     int
+        :param follow:          If the kind of isolation is a "follow" one
+        :type follow:           bool
+        :param nr_passes:       Number of passes
+        :type nr_passes:        int
+        :return:                The buffered geometry
+        :rtype:                 MultiPolygon or Polygon
+        """
+
+        if follow:
+            geom = self.grb_obj.isolation_geometry(offset, geometry=geometry, follow=follow)
+        else:
+            try:
+                geom = self.grb_obj.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type,
+                                                       passes=nr_passes)
+            except Exception as e:
+                log.debug('ToolIsolation.isolate().generate_envelope() --> %s' % str(e))
+                return 'fail'
 
         if invert:
             try:
-                try:
-                    pl = []
-                    for p in geom:
-                        if p is not None:
-                            if isinstance(p, Polygon):
-                                pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
-                            elif isinstance(p, LinearRing):
-                                pl.append(Polygon(p.coords[::-1]))
-                    geom = MultiPolygon(pl)
-                except TypeError:
-                    if isinstance(geom, Polygon) and geom is not None:
-                        geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
-                    elif isinstance(geom, LinearRing) and geom is not None:
-                        geom = Polygon(geom.coords[::-1])
-                    else:
-                        log.debug("NonCopperClear.generate_envelope() Error --> Unexpected Geometry %s" %
-                                  type(geom))
+                pl = []
+                for p in geom:
+                    if p is not None:
+                        if isinstance(p, Polygon):
+                            pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                        elif isinstance(p, LinearRing):
+                            pl.append(Polygon(p.coords[::-1]))
+                geom = MultiPolygon(pl)
+            except TypeError:
+                if isinstance(geom, Polygon) and geom is not None:
+                    geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+                elif isinstance(geom, LinearRing) and geom is not None:
+                    geom = Polygon(geom.coords[::-1])
+                else:
+                    log.debug("ToolIsolation.generate_envelope() Error --> Unexpected Geometry %s" %
+                              type(geom))
             except Exception as e:
-                log.debug("NonCopperClear.generate_envelope() Error --> %s" % str(e))
+                log.debug("ToolIsolation.generate_envelope() Error --> %s" % str(e))
                 return 'fail'
         return geom
 

+ 4 - 0
CHANGELOG.md

@@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+27.05.2020
+
+- working on Isolation Tool: made to work the Isolation with multiple tools without rest machining
+
 26.05.2020
 
 - working on Isolation Tool: made to work the tool parameters data to GUI and GUI to data