Explorar o código

- In Gerber isolation changed the UI
- in Gerber isolation added the option to selectively isolate only certain polygons

Marius Stanciu %!s(int64=6) %!d(string=hai) anos
pai
achega
d5a9e0bb5a
Modificáronse 6 ficheiros con 325 adicións e 222 borrados
  1. 11 4
      FlatCAMApp.py
  2. 133 89
      FlatCAMObj.py
  3. 5 0
      README.md
  4. 47 62
      camlib.py
  5. 78 54
      flatcamGUI/ObjectUI.py
  6. 51 13
      flatcamGUI/PreferencesUI.py

+ 11 - 4
FlatCAMApp.py

@@ -523,6 +523,7 @@ class App(QtCore.QObject):
             "gerber_isooverlap": 0.00393701,
             "gerber_isooverlap": 0.00393701,
             "gerber_milling_type": "cl",
             "gerber_milling_type": "cl",
             "gerber_combine_passes": False,
             "gerber_combine_passes": False,
+            "gerber_iso_scope": 'all',
             "gerber_noncoppermargin": 0.00393701,
             "gerber_noncoppermargin": 0.00393701,
             "gerber_noncopperrounded": False,
             "gerber_noncopperrounded": False,
             "gerber_bboxmargin": 0.00393701,
             "gerber_bboxmargin": 0.00393701,
@@ -537,6 +538,7 @@ class App(QtCore.QObject):
             "gerber_vtipdia": 0.1,
             "gerber_vtipdia": 0.1,
             "gerber_vtipangle": 30,
             "gerber_vtipangle": 30,
             "gerber_vcutz": -0.05,
             "gerber_vcutz": -0.05,
+            "gerber_iso_type": "full",
             "gerber_buffering": "full",
             "gerber_buffering": "full",
             "gerber_simplification": False,
             "gerber_simplification": False,
             "gerber_simp_tolerance": 0.0005,
             "gerber_simp_tolerance": 0.0005,
@@ -1077,6 +1079,7 @@ class App(QtCore.QObject):
             "gerber_isopasses": self.ui.gerber_defaults_form.gerber_opt_group.iso_width_entry,
             "gerber_isopasses": self.ui.gerber_defaults_form.gerber_opt_group.iso_width_entry,
             "gerber_isooverlap": self.ui.gerber_defaults_form.gerber_opt_group.iso_overlap_entry,
             "gerber_isooverlap": self.ui.gerber_defaults_form.gerber_opt_group.iso_overlap_entry,
             "gerber_combine_passes": self.ui.gerber_defaults_form.gerber_opt_group.combine_passes_cb,
             "gerber_combine_passes": self.ui.gerber_defaults_form.gerber_opt_group.combine_passes_cb,
+            "gerber_iso_scope": self.ui.gerber_defaults_form.gerber_opt_group.iso_scope_radio,
             "gerber_milling_type": self.ui.gerber_defaults_form.gerber_opt_group.milling_type_radio,
             "gerber_milling_type": self.ui.gerber_defaults_form.gerber_opt_group.milling_type_radio,
             "gerber_noncoppermargin": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_margin_entry,
             "gerber_noncoppermargin": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_margin_entry,
             "gerber_noncopperrounded": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_rounded_cb,
             "gerber_noncopperrounded": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_rounded_cb,
@@ -1092,6 +1095,7 @@ class App(QtCore.QObject):
             "gerber_vtipdia": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipdia_spinner,
             "gerber_vtipdia": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipdia_spinner,
             "gerber_vtipangle": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipangle_spinner,
             "gerber_vtipangle": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipangle_spinner,
             "gerber_vcutz": self.ui.gerber_defaults_form.gerber_adv_opt_group.cutz_spinner,
             "gerber_vcutz": self.ui.gerber_defaults_form.gerber_adv_opt_group.cutz_spinner,
+            "gerber_iso_type": self.ui.gerber_defaults_form.gerber_adv_opt_group.iso_type_radio,
 
 
             "gerber_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffering_radio,
             "gerber_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffering_radio,
             "gerber_simplification": self.ui.gerber_defaults_form.gerber_adv_opt_group.simplify_cb,
             "gerber_simplification": self.ui.gerber_defaults_form.gerber_adv_opt_group.simplify_cb,
@@ -2424,6 +2428,9 @@ class App(QtCore.QObject):
         # decide if we have a double click or single click
         # decide if we have a double click or single click
         self.doubleclick = False
         self.doubleclick = False
 
 
+        # store here the is_dragging value
+        self.event_is_dragging = False
+
         # variable to store if a command is active (then the var is not None) and which one it is
         # variable to store if a command is active (then the var is not None) and which one it is
         self.command_active = None
         self.command_active = None
         # variable to store the status of moving selection action
         # variable to store the status of moving selection action
@@ -8337,7 +8344,7 @@ class App(QtCore.QObject):
                 pan_button = 2
                 pan_button = 2
             else:
             else:
                 pan_button = 3
                 pan_button = 3
-            event_is_dragging = event.is_dragging
+            self.event_is_dragging = event.is_dragging
         else:
         else:
             event_pos = (event.xdata, event.ydata)
             event_pos = (event.xdata, event.ydata)
             # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy
             # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy
@@ -8345,7 +8352,7 @@ class App(QtCore.QObject):
                 pan_button = 3
                 pan_button = 3
             else:
             else:
                 pan_button = 2
                 pan_button = 2
-            event_is_dragging = self.plotcanvas.is_dragging
+            self.event_is_dragging = self.plotcanvas.is_dragging
 
 
         # So it can receive key presses
         # So it can receive key presses
         self.plotcanvas.native.setFocus()
         self.plotcanvas.native.setFocus()
@@ -8355,7 +8362,7 @@ class App(QtCore.QObject):
 
 
         if not origin_click:
         if not origin_click:
             # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
             # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-            if event.button == pan_button and event_is_dragging == 1:
+            if event.button == pan_button and self.event_is_dragging == 1:
                 self.ui.popMenu.mouse_is_panning = True
                 self.ui.popMenu.mouse_is_panning = True
                 return
                 return
 
 
@@ -8383,7 +8390,7 @@ class App(QtCore.QObject):
                 self.mouse = [pos[0], pos[1]]
                 self.mouse = [pos[0], pos[1]]
 
 
                 # if the mouse is moved and the LMB is clicked then the action is a selection
                 # if the mouse is moved and the LMB is clicked then the action is a selection
-                if event_is_dragging == 1 and event.button == 1:
+                if self.event_is_dragging == 1 and event.button == 1:
                     self.delete_selection_shape()
                     self.delete_selection_shape()
                     if dx < 0:
                     if dx < 0:
                         self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
                         self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],

+ 133 - 89
FlatCAMObj.py

@@ -597,7 +597,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "bboxmargin": 0.0,
             "bboxmargin": 0.0,
             "bboxrounded": False,
             "bboxrounded": False,
             "aperture_display": False,
             "aperture_display": False,
-            "follow": False
+            "follow": False,
+            "iso_scope": 'all',
+            "iso_type": 'full'
         })
         })
 
 
         # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
         # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
@@ -618,6 +620,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # Number of decimals to be used by tools in this class
         # Number of decimals to be used by tools in this class
         self.decimals = 4
         self.decimals = 4
 
 
+        # Mouse events
+        self.mr = self.app.mr
+
+        # list to store the polygons selected for isolation
+        self.poly_list = list()
+
         # 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.
@@ -662,7 +670,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "bboxmargin": self.ui.bbmargin_entry,
             "bboxmargin": self.ui.bbmargin_entry,
             "bboxrounded": self.ui.bbrounded_cb,
             "bboxrounded": self.ui.bbrounded_cb,
             "aperture_display": self.ui.aperture_table_visibility_cb,
             "aperture_display": self.ui.aperture_table_visibility_cb,
-            "follow": self.ui.follow_cb
+            "follow": self.ui.follow_cb,
+            "iso_scope": self.ui.iso_scope_radio,
+            "iso_type": self.ui.iso_type_radio
         })
         })
 
 
         # Fill form fields only on object create
         # Fill form fields only on object create
@@ -672,8 +682,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
         self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
-        self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click)
-        self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click)
         self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
         self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
         self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
         self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
         self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
         self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
@@ -710,8 +718,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             self.ui.aperture_table_visibility_cb.hide()
             self.ui.aperture_table_visibility_cb.hide()
             self.ui.milling_type_label.hide()
             self.ui.milling_type_label.hide()
             self.ui.milling_type_radio.hide()
             self.ui.milling_type_radio.hide()
-            self.ui.generate_ext_iso_button.hide()
-            self.ui.generate_int_iso_button.hide()
+            self.ui.iso_type_radio.hide()
+
             self.ui.follow_cb.hide()
             self.ui.follow_cb.hide()
             self.ui.except_cb.setChecked(False)
             self.ui.except_cb.setChecked(False)
             self.ui.except_cb.hide()
             self.ui.except_cb.hide()
@@ -978,42 +986,16 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         self.app.new_object("geometry", name, geo_init)
         self.app.new_object("geometry", name, geo_init)
 
 
-    def on_ext_iso_button_click(self, *args):
-        obj = self.app.collection.get_active()
-
-        def worker_task(obj, app_obj):
-            with self.app.proc_container.new(_("Isolating...")):
-                if self.ui.follow_cb.get_value() is True:
-                    obj.follow_geo()
-                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
-                    obj.ui.plot_cb.toggle()
-                else:
-                    app_obj.report_usage("gerber_on_iso_button")
-                    self.read_form()
-                    self.isolate(iso_type=0)
-
-        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
-
-    def on_int_iso_button_click(self, *args):
-        obj = self.app.collection.get_active()
-
-        def worker_task(obj, app_obj):
-            with self.app.proc_container.new(_("Isolating...")):
-                if self.ui.follow_cb.get_value() is True:
-                    obj.follow_geo()
-                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
-                    obj.ui.plot_cb.toggle()
-                else:
-                    app_obj.report_usage("gerber_on_iso_button")
-                    self.read_form()
-                    self.isolate(iso_type=1)
-
-        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
-
     def on_iso_button_click(self, *args):
     def on_iso_button_click(self, *args):
 
 
         obj = self.app.collection.get_active()
         obj = self.app.collection.get_active()
 
 
+        self.iso_type = 2
+        if self.ui.iso_type_radio.get_value() == 'ext':
+            self.iso_type = 0
+        if self.ui.iso_type_radio.get_value() == 'int':
+            self.iso_type = 1
+
         def worker_task(obj, app_obj):
         def worker_task(obj, app_obj):
             with self.app.proc_container.new(_("Isolating...")):
             with self.app.proc_container.new(_("Isolating...")):
                 if self.ui.follow_cb.get_value() is True:
                 if self.ui.follow_cb.get_value() is True:
@@ -1023,7 +1005,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 else:
                 else:
                     app_obj.report_usage("gerber_on_iso_button")
                     app_obj.report_usage("gerber_on_iso_button")
                     self.read_form()
                     self.read_form()
-                    self.isolate()
+
+                    iso_scope = 'all' if self.ui.iso_scope_radio == 'all' else 'single'
+                    self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
 
 
         self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
         self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
 
 
@@ -1053,18 +1037,96 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         except Exception as e:
         except Exception as e:
             return "Operation failed: %s" % str(e)
             return "Operation failed: %s" % str(e)
 
 
-    def isolate(self, iso_type=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
+    def isolate_handler(self, iso_type, iso_scope):
+
+        if iso_scope == 'all':
+            self.isolate(iso_type=iso_type)
+        else:
+            # disengage the grid snapping since it will be hard to find the drills on grid
+            if self.app.ui.grid_snap_btn.isChecked():
+                self.grid_status_memory = True
+                self.app.ui.grid_snap_btn.trigger()
+            else:
+                self.grid_status_memory = False
+
+            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on polygon to isolate it."))
+
+    def on_mouse_click_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
+
+        try:
+            x = float(event_pos[0])
+            y = float(event_pos[1])
+        except TypeError:
+            return
+
+        event_pos = (x, y)
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        if event.button == 1:
+            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]))
+
+            if clicked_poly:
+                self.poly_list.append(clicked_poly)
+                self.app.inform.emit(
+                    '%s: %d. %s' % (_("Added polygon"),
+                                    int(len(self.poly_list)),
+                                    _("Click to start adding next polygon or right click to start isolation."))
+                )
+            else:
+                self.app.inform.emit(_("No polygon detected under click position. Try again."))
+
+        elif event.button == right_button and self.app.event_is_dragging is False:
+            # restore the Grid snapping if it was active before
+            if self.grid_status_memory is True:
+                self.app.ui.grid_snap_btn.trigger()
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            self.isolate(iso_type=self.iso_type, geometry=self.poly_list)
+
+    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):
                 milling_type=None, follow=None, plot=True):
         """
         """
         Creates an isolation routing geometry object in the project.
         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 iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
+        :param iso_scope: whether to isolate all polygons or single polygpns: 'all' = all, 'single' = one by one, single
         :param dia: Tool diameter
         :param dia: Tool diameter
         :param passes: Number of tool widths to cut
         :param passes: Number of tool widths to cut
         :param overlap: Overlap between passes in fraction of tool diameter
         :param overlap: Overlap between passes in fraction of tool diameter
         :param outname: Base name of the output object
         :param outname: Base name of the output object
         :return: None
         :return: None
         """
         """
+
+        if geometry is None:
+            if follow:
+                work_geo = self.follow_geometry
+            else:
+                work_geo = self.solid_geometry
+        else:
+            work_geo = geometry
+
         if dia is None:
         if dia is None:
             dia = float(self.options["isotooldia"])
             dia = float(self.options["isotooldia"])
         if passes is None:
         if passes is None:
@@ -1075,28 +1137,33 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             combine = self.options["combine_passes"]
             combine = self.options["combine_passes"]
         else:
         else:
             combine = bool(combine)
             combine = bool(combine)
+
         if milling_type is None:
         if milling_type is None:
             milling_type = self.options["milling_type"]
             milling_type = self.options["milling_type"]
 
 
         if iso_type is None:
         if iso_type is None:
-            self.iso_type = 2
+            iso_t = 2
         else:
         else:
-            self.iso_type = iso_type
+            iso_t = iso_type
 
 
         base_name = self.options["name"]
         base_name = self.options["name"]
 
 
-        def generate_envelope(offset, invert, envelope_iso_type=2, follow=None, passes=0):
+        def generate_envelope(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
             # 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 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 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 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
             # 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.
             # 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, passes=passes)
-            except Exception as e:
-                log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e))
-                return 'fail'
+
+            if follow:
+                geom = self.isolation_geometry(offset, geometry=geometry, follow=follow)
+            else:
+                try:
+                    geom = self.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type, passes=nr_passes)
+                except Exception as e:
+                    log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e))
+                    return 'fail'
 
 
             if invert:
             if invert:
                 try:
                 try:
@@ -1121,24 +1188,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     return 'fail'
                     return 'fail'
             return geom
             return geom
 
 
-            # if invert:
-            #     try:
-            #         if type(geom) is MultiPolygon:
-            #             pl = []
-            #             for p in geom:
-            #                 if p is not None:
-            #                     pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
-            #             geom = MultiPolygon(pl)
-            #         elif type(geom) is Polygon and geom is not None:
-            #             geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
-            #         else:
-            #             log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
-            #                       type(geom))
-            #     except Exception as e:
-            #         log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
-            #         return 'fail'
-            # return geom
-
         # if float(self.options["isotooldia"]) < 0:
         # if float(self.options["isotooldia"]) < 0:
         #     self.options["isotooldia"] = -self.options["isotooldia"]
         #     self.options["isotooldia"] = -self.options["isotooldia"]
 
 
@@ -1210,30 +1259,26 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
                     iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
 
 
                     # if milling type is climb then the move is counter-clockwise around features
                     # if milling type is climb then the move is counter-clockwise around features
-                    if milling_type == 'cl':
-                        # geom = generate_envelope (offset, i == 0)
-                        geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow,
-                                                 passes=i)
-                    else:
-                        geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow,
-                                                 passes=i)
+                    mill_t = 1 if milling_type == 'cl' else 0
+                    geom = generate_envelope(iso_offset, mill_t, geometry=work_geo, env_iso_type=iso_t, follow=follow,
+                                             nr_passes=i)
+
                     if geom == 'fail':
                     if geom == 'fail':
-                        app_obj.inform.emit('[ERROR_NOTCL] %s' %
-                                            _("Isolation geometry could not be generated."))
+                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
                         return 'fail'
                         return 'fail'
                     geo_obj.solid_geometry.append(geom)
                     geo_obj.solid_geometry.append(geom)
 
 
                     # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
                     # 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() == 'circular':
-                        new_cutz = self.app.defaults['geometry_cutz']
-                        new_vtipdia = self.app.defaults['geometry_vtipdia']
-                        new_vtipangle = self.app.defaults['geometry_vtipangle']
-                        tool_type = 'C1'
-                    else:
+                    if self.ui.tool_type_radio.get_value() == 'v':
                         new_cutz = self.ui.cutz_spinner.get_value()
                         new_cutz = self.ui.cutz_spinner.get_value()
                         new_vtipdia = self.ui.tipdia_spinner.get_value()
                         new_vtipdia = self.ui.tipdia_spinner.get_value()
                         new_vtipangle = self.ui.tipangle_spinner.get_value()
                         new_vtipangle = self.ui.tipangle_spinner.get_value()
                         tool_type = 'V'
                         tool_type = 'V'
+                    else:
+                        new_cutz = self.app.defaults['geometry_cutz']
+                        new_vtipdia = self.app.defaults['geometry_vtipdia']
+                        new_vtipangle = self.app.defaults['geometry_vtipangle']
+                        tool_type = 'C1'
 
 
                     # store here the default data for Geometry Data
                     # store here the default data for Geometry Data
                     default_data = {}
                     default_data = {}
@@ -1280,7 +1325,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 # proceed with object creation, if there are empty and the number of them is the length
                 # 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
                 # of the list then we have an empty solid_geometry which should raise a Custom Exception
                 empty_cnt = 0
                 empty_cnt = 0
-                if not isinstance(geo_obj.solid_geometry, list):
+                if not isinstance(geo_obj.solid_geometry, list) and \
+                        not isinstance(geo_obj.solid_geometry, MultiPolygon):
                     geo_obj.solid_geometry = [geo_obj.solid_geometry]
                     geo_obj.solid_geometry = [geo_obj.solid_geometry]
 
 
                 for g in geo_obj.solid_geometry:
                 for g in geo_obj.solid_geometry:
@@ -1338,13 +1384,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
                     geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
 
 
                     # if milling type is climb then the move is counter-clockwise around features
                     # if milling type is climb then the move is counter-clockwise around features
-                    if milling_type == 'cl':
-                        # geo_obj.solid_geometry = generate_envelope(offset, i == 0)
-                        geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow,
-                                                 passes=i)
-                    else:
-                        geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow,
-                                                 passes=i)
+                    mill_t = 1 if milling_type == 'cl' else 0
+                    mill_t = 1 if milling_type == 'cl' else 0
+                    geom = generate_envelope(offset, mill_t, geometry=work_geo, env_iso_type=iso_t, follow=follow,
+                                             nr_passes=i)
+
                     if geom == 'fail':
                     if geom == 'fail':
                         app_obj.inform.emit('[ERROR_NOTCL] %s' %
                         app_obj.inform.emit('[ERROR_NOTCL] %s' %
                                             _("Isolation geometry could not be generated."))
                                             _("Isolation geometry could not be generated."))

+ 5 - 0
README.md

@@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+25.11.2019
+
+- In Gerber isolation changed the UI
+- in Gerber isolation added the option to selectively isolate only certain polygons
+
 23.11.2019
 23.11.2019
 
 
 - in Tool Fiducials added a new fiducial type: chess pattern
 - in Tool Fiducials added a new fiducial type: chess pattern

+ 47 - 62
camlib.py

@@ -897,7 +897,7 @@ class Geometry(object):
     #
     #
     #     return self.flat_geometry, self.flat_geometry_rtree
     #     return self.flat_geometry, self.flat_geometry_rtree
 
 
-    def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None, passes=0):
+    def isolation_geometry(self, offset, geometry=None, iso_type=2, corner=None, follow=None, passes=0):
         """
         """
         Creates contours around geometry at a given
         Creates contours around geometry at a given
         offset distance.
         offset distance.
@@ -916,73 +916,58 @@ class Geometry(object):
             # graceful abort requested by the user
             # graceful abort requested by the user
             raise FlatCAMApp.GracefulException
             raise FlatCAMApp.GracefulException
 
 
-        geo_iso = []
-        if offset == 0:
-            if follow:
-                geo_iso = self.follow_geometry
-            else:
-                geo_iso = self.solid_geometry
+        geo_iso = list()
+
+        if follow:
+            return geometry
+
+        if geometry:
+            working_geo = geometry
         else:
         else:
-            if follow:
-                geo_iso = self.follow_geometry
+            working_geo = self.solid_geometry
+
+        try:
+            geo_len = len(working_geo)
+        except TypeError:
+            geo_len = 1
+
+        old_disp_number = 0
+        pol_nr = 0
+        # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo
+        try:
+            for pol in working_geo:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+                if offset == 0:
+                    geo_iso.append(pol)
+                else:
+                    corner_type = 1 if corner is None else corner
+                    geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner_type))
+                pol_nr += 1
+                disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                if  old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %s %d: %d%%' %
+                                                             (_("Pass"), int(passes + 1), int(disp_number)))
+                    old_disp_number = disp_number
+        except TypeError:
+            # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+            # MultiPolygon (not an iterable)
+            if offset == 0:
+                geo_iso.append(working_geo)
             else:
             else:
-                # if isinstance(self.solid_geometry, list):
-                #     temp_geo = cascaded_union(self.solid_geometry)
-                # else:
-                #     temp_geo = self.solid_geometry
-
-                # Remember: do not make a buffer for each element in the solid_geometry because it will cut into
-                # other copper features
-                # if corner is None:
-                #     geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
-                # else:
-                #     geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                #                               join_style=corner)
+                corner_type = 1 if corner is None else corner
+                geo_iso.append(working_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
+                                                  join_style=corner_type))
 
 
-                # variables to display the percentage of work done
-                geo_len = 0
-                try:
-                    for pol in self.solid_geometry:
-                        geo_len += 1
-                except TypeError:
-                    geo_len = 1
-                disp_number = 0
-                old_disp_number = 0
-                pol_nr = 0
-                # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo
-                try:
-                    for pol in self.solid_geometry:
-                        if self.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise FlatCAMApp.GracefulException
-                        if corner is None:
-                            geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-                        else:
-                            geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4)),
-                                           join_style=corner)
-                        pol_nr += 1
-                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
-
-                        if  old_disp_number < disp_number <= 100:
-                            self.app.proc_container.update_view_text(' %s %d: %d%%' %
-                                                                     (_("Pass"), int(passes + 1), int(disp_number)))
-                            old_disp_number = disp_number
-                except TypeError:
-                    # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
-                    # MultiPolygon (not an iterable)
-                    if corner is None:
-                        geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-                    else:
-                        geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)),
-                                       join_style=corner)
-                self.app.proc_container.update_view_text(' %s' % _("Buffering"))
-                geo_iso = unary_union(geo_iso)
+        self.app.proc_container.update_view_text(' %s' % _("Buffering"))
+        geo_iso = unary_union(geo_iso)
 
 
         self.app.proc_container.update_view_text('')
         self.app.proc_container.update_view_text('')
         # end of replaced block
         # end of replaced block
-        if follow:
-            return geo_iso
-        elif iso_type == 2:
+
+        if iso_type == 2:
             return geo_iso
             return geo_iso
         elif iso_type == 0:
         elif iso_type == 0:
             self.app.proc_container.update_view_text(' %s' % _("Get Exteriors"))
             self.app.proc_container.update_view_text(' %s' % _("Get Exteriors"))

+ 78 - 54
flatcamGUI/ObjectUI.py

@@ -281,6 +281,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid1)
         self.custom_box.addLayout(grid1)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(1, 1)
         grid1.setColumnStretch(1, 1)
+        grid1.setColumnStretch(2, 1)
 
 
         # Tool Type
         # Tool Type
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
         self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
@@ -290,8 +291,8 @@ class GerberObjectUI(ObjectUI):
               "When the 'V-shape' is selected then the tool\n"
               "When the 'V-shape' is selected then the tool\n"
               "diameter will depend on the chosen cut depth.")
               "diameter will depend on the chosen cut depth.")
         )
         )
-        self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'},
-                                         {'label': 'V-Shape', 'value': 'v'}])
+        self.tool_type_radio = RadioSet([{'label': _('Circular'), 'value': 'circular'},
+                                         {'label': _('V-Shape'), 'value': 'v'}])
 
 
         grid1.addWidget(self.tool_type_label, 0, 0)
         grid1.addWidget(self.tool_type_label, 0, 0)
         grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2)
         grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2)
@@ -396,7 +397,7 @@ class GerberObjectUI(ObjectUI):
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
 
 
         # combine all passes CB
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
         self.combine_passes_cb.setToolTip(
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
             _("Combine all passes into one object")
         )
         )
@@ -406,15 +407,15 @@ class GerberObjectUI(ObjectUI):
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
                                     "This means that it will cut through\n"
                                     "This means that it will cut through\n"
                                     "the middle of the trace."))
                                     "the middle of the trace."))
+        grid1.addWidget(self.combine_passes_cb, 8, 0)
 
 
         # avoid an area from isolation
         # avoid an area from isolation
         self.except_cb = FCCheckBox(label=_('Except'))
         self.except_cb = FCCheckBox(label=_('Except'))
+        grid1.addWidget(self.follow_cb, 8, 1)
+
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object bellow\n"
                                     "by checking this, the area of the object bellow\n"
                                     "will be subtracted from the isolation geometry."))
                                     "will be subtracted from the isolation geometry."))
-
-        grid1.addWidget(self.combine_passes_cb, 8, 0)
-        grid1.addWidget(self.follow_cb, 8, 1)
         grid1.addWidget(self.except_cb, 8, 2)
         grid1.addWidget(self.except_cb, 8, 2)
 
 
         # ## Form Layout
         # ## Form Layout
@@ -454,8 +455,50 @@ class GerberObjectUI(ObjectUI):
 
 
         form_layout.addRow(self.obj_label, self.obj_combo)
         form_layout.addRow(self.obj_label, self.obj_combo)
 
 
-        self.gen_iso_label = QtWidgets.QLabel("<b>%s</b>" % _("Generate Isolation Geometry"))
-        self.gen_iso_label.setToolTip(
+        # ---------------------------------------------- #
+        # --------- Isolation scope -------------------- #
+        # ---------------------------------------------- #
+        self.iso_scope_label = QtWidgets.QLabel('%s:' % _('Scope'))
+        self.iso_scope_label.setToolTip(
+            _("Isolation scope. Choose what to isolate:\n"
+              "- 'All' -> Isolate all the polygons in the object\n"
+              "- 'Single' -> Isolate a single polygon.")
+        )
+        self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'},
+                                         {'label': _('Single'), 'value': 'single'}])
+
+        grid1.addWidget(self.iso_scope_label, 10, 0)
+        grid1.addWidget(self.iso_scope_radio, 10, 1, 1, 2)
+
+        # ---------------------------------------------- #
+        # --------- Isolation type  -------------------- #
+        # ---------------------------------------------- #
+        self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
+        self.iso_type_label.setToolTip(
+            _("Choose how the isolation will be executed:\n"
+              "- 'Full' -> complete isolation of polygons\n"
+              "- 'Ext' -> will isolate only on the outside\n"
+              "- 'Int' -> will isolate only on the inside\n"
+              "'Exterior' isolation is almost always possible\n"
+              "(with the right tool) but 'Interior'\n"
+              "isolation can be done only when there is an opening\n"
+              "inside of the polygon (e.g polygon is a 'doughnut' shape).")
+        )
+        self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
+                                        {'label': _('Ext'), 'value': 'ext'},
+                                        {'label': _('Int'), 'value': 'int'}])
+
+        grid1.addWidget(self.iso_type_label, 11, 0)
+        grid1.addWidget(self.iso_type_radio, 11, 1, 1, 2)
+
+        self.generate_iso_button = QtWidgets.QPushButton("%s" % _("Generate Isolation Geometry"))
+        self.generate_iso_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.generate_iso_button.setToolTip(
             _("Create a Geometry object with toolpaths to cut \n"
             _("Create a Geometry object with toolpaths to cut \n"
               "isolation outside, inside or on both sides of the\n"
               "isolation outside, inside or on both sides of the\n"
               "object. For a Gerber object outside means outside\n"
               "object. For a Gerber object outside means outside\n"
@@ -466,7 +509,7 @@ class GerberObjectUI(ObjectUI):
               "inside the actual Gerber feature, use a negative tool\n"
               "inside the actual Gerber feature, use a negative tool\n"
               "diameter above.")
               "diameter above.")
         )
         )
-        grid1.addWidget(self.gen_iso_label, 10, 0, 1, 3)
+        grid1.addWidget(self.generate_iso_button, 12, 0, 1, 3)
 
 
         self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry'))
         self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry'))
         self.create_buffer_button.setToolTip(
         self.create_buffer_button.setToolTip(
@@ -475,48 +518,15 @@ class GerberObjectUI(ObjectUI):
               "Clicking this will create the buffered geometry\n"
               "Clicking this will create the buffered geometry\n"
               "required for isolation.")
               "required for isolation.")
         )
         )
-        grid1.addWidget(self.create_buffer_button, 11, 0, 1, 3)
-
-        self.generate_iso_button = QtWidgets.QPushButton(_('FULL Geo'))
-        self.generate_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing. It contains both\n"
-              "the interiors and exteriors geometry.")
-        )
-        grid1.addWidget(self.generate_iso_button, 12, 0)
-
-        hlay_1 = QtWidgets.QHBoxLayout()
-        grid1.addLayout(hlay_1, 12, 1, 1, 2)
-
-        self.generate_ext_iso_button = QtWidgets.QPushButton(_('Ext Geo'))
-        self.generate_ext_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing containing\n"
-              "only the exteriors geometry.")
-        )
-        # self.generate_ext_iso_button.setMinimumWidth(100)
-        hlay_1.addWidget(self.generate_ext_iso_button)
-
-        self.generate_int_iso_button = QtWidgets.QPushButton(_('Int Geo'))
-        self.generate_int_iso_button.setToolTip(
-            _("Create the Geometry Object\n"
-              "for isolation routing containing\n"
-              "only the interiors geometry.")
-        )
-        # self.generate_ext_iso_button.setMinimumWidth(90)
-        hlay_1.addWidget(self.generate_int_iso_button)
+        grid1.addWidget(self.create_buffer_button, 13, 0, 1, 2)
 
 
         self.ohis_iso = OptionalHideInputSection(
         self.ohis_iso = OptionalHideInputSection(
             self.except_cb,
             self.except_cb,
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             logic=True
             logic=True
         )
         )
-        # when the follow checkbox is checked then the exteriors and interiors isolation generation buttons
-        # are disabled as is doesn't make sense to have them enabled due of the nature of "follow"
-        self.ois_iso = OptionalInputSection(self.follow_cb,
-                                            [self.generate_int_iso_button, self.generate_ext_iso_button], logic=False)
 
 
-        grid1.addWidget(QtWidgets.QLabel(''), 13, 0)
+        grid1.addWidget(QtWidgets.QLabel(''), 14, 0)
 
 
         # ###########################################
         # ###########################################
         # ########## NEW GRID #######################
         # ########## NEW GRID #######################
@@ -562,6 +572,11 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.board_cutout_label, 2, 0)
         grid2.addWidget(self.board_cutout_label, 2, 0)
         grid2.addWidget(self.generate_cutout_button, 2, 1)
         grid2.addWidget(self.generate_cutout_button, 2, 1)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 3, 0, 1, 2)
+
         # ## Non-copper regions
         # ## Non-copper regions
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label.setToolTip(
         self.noncopper_label.setToolTip(
@@ -572,7 +587,7 @@ class GerberObjectUI(ObjectUI):
               "copper from a specified region.")
               "copper from a specified region.")
         )
         )
 
 
-        grid2.addWidget(self.noncopper_label, 3, 0, 1, 2)
+        grid2.addWidget(self.noncopper_label, 4, 0, 1, 2)
 
 
         # Margin
         # Margin
         bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
@@ -588,8 +603,8 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_margin_entry.set_precision(self.decimals)
         self.noncopper_margin_entry.set_precision(self.decimals)
         self.noncopper_margin_entry.setSingleStep(0.1)
         self.noncopper_margin_entry.setSingleStep(0.1)
 
 
-        grid2.addWidget(bmlabel, 4, 0)
-        grid2.addWidget(self.noncopper_margin_entry, 4, 1)
+        grid2.addWidget(bmlabel, 5, 0)
+        grid2.addWidget(self.noncopper_margin_entry, 5, 1)
 
 
         # Rounded corners
         # Rounded corners
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
@@ -599,8 +614,13 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_rounded_cb.setMinimumWidth(90)
         self.noncopper_rounded_cb.setMinimumWidth(90)
 
 
         self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo'))
         self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo'))
-        grid2.addWidget(self.noncopper_rounded_cb, 5, 0)
-        grid2.addWidget(self.generate_noncopper_button, 5, 1)
+        grid2.addWidget(self.noncopper_rounded_cb, 6, 0)
+        grid2.addWidget(self.generate_noncopper_button, 6, 1)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line1, 7, 0, 1, 2)
 
 
         # ## Bounding box
         # ## Bounding box
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
@@ -609,7 +629,7 @@ class GerberObjectUI(ObjectUI):
               "Square shape.")
               "Square shape.")
         )
         )
 
 
-        grid2.addWidget(self.boundingbox_label, 6, 0, 1, 2)
+        grid2.addWidget(self.boundingbox_label, 8, 0, 1, 2)
 
 
         bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin.setToolTip(
         bbmargin.setToolTip(
@@ -622,8 +642,8 @@ class GerberObjectUI(ObjectUI):
         self.bbmargin_entry.set_precision(self.decimals)
         self.bbmargin_entry.set_precision(self.decimals)
         self.bbmargin_entry.setSingleStep(0.1)
         self.bbmargin_entry.setSingleStep(0.1)
 
 
-        grid2.addWidget(bbmargin, 7, 0)
-        grid2.addWidget(self.bbmargin_entry, 7, 1)
+        grid2.addWidget(bbmargin, 9, 0)
+        grid2.addWidget(self.bbmargin_entry, 9, 1)
 
 
         self.bbrounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.bbrounded_cb = FCCheckBox(label=_("Rounded Geo"))
         self.bbrounded_cb.setToolTip(
         self.bbrounded_cb.setToolTip(
@@ -638,9 +658,13 @@ class GerberObjectUI(ObjectUI):
         self.generate_bb_button.setToolTip(
         self.generate_bb_button.setToolTip(
             _("Generate the Geometry object.")
             _("Generate the Geometry object.")
         )
         )
-        grid2.addWidget(self.bbrounded_cb, 8, 0)
-        grid2.addWidget(self.generate_bb_button, 8, 1)
+        grid2.addWidget(self.bbrounded_cb, 10, 0)
+        grid2.addWidget(self.generate_bb_button, 10, 1)
 
 
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line2, 11, 0, 1, 2)
 
 
 class ExcellonObjectUI(ObjectUI):
 class ExcellonObjectUI(ObjectUI):
     """
     """

+ 51 - 13
flatcamGUI/PreferencesUI.py

@@ -1344,9 +1344,10 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
             _("Width of the isolation gap in\n"
             _("Width of the isolation gap in\n"
               "number (integer) of tool widths.")
               "number (integer) of tool widths.")
         )
         )
-        grid0.addWidget(passlabel, 1, 0)
         self.iso_width_entry = FCSpinner()
         self.iso_width_entry = FCSpinner()
         self.iso_width_entry.setRange(1, 999)
         self.iso_width_entry.setRange(1, 999)
+
+        grid0.addWidget(passlabel, 1, 0)
         grid0.addWidget(self.iso_width_entry, 1, 1)
         grid0.addWidget(self.iso_width_entry, 1, 1)
 
 
         # Pass overlap
         # Pass overlap
@@ -1356,14 +1357,28 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
               "Example:\n"
               "Example:\n"
               "A value here of 0.25 means an overlap of 25%% from the tool diameter found above.")
               "A value here of 0.25 means an overlap of 25%% from the tool diameter found above.")
         )
         )
-        grid0.addWidget(overlabel, 2, 0)
         self.iso_overlap_entry = FCDoubleSpinner()
         self.iso_overlap_entry = FCDoubleSpinner()
         self.iso_overlap_entry.set_precision(3)
         self.iso_overlap_entry.set_precision(3)
         self.iso_overlap_entry.setWrapping(True)
         self.iso_overlap_entry.setWrapping(True)
         self.iso_overlap_entry.setRange(0.000, 0.999)
         self.iso_overlap_entry.setRange(0.000, 0.999)
         self.iso_overlap_entry.setSingleStep(0.1)
         self.iso_overlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(overlabel, 2, 0)
         grid0.addWidget(self.iso_overlap_entry, 2, 1)
         grid0.addWidget(self.iso_overlap_entry, 2, 1)
 
 
+        # Isolation Scope
+        self.iso_scope_label = QtWidgets.QLabel('%s:' % _('Scope'))
+        self.iso_scope_label.setToolTip(
+            _("Isolation scope. Choose what to isolate:\n"
+              "- 'All' -> Isolate all the polygons in the object\n"
+              "- 'Single' -> Isolate a single polygon.")
+        )
+        self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'},
+                                         {'label': _('Single'), 'value': 'single'}])
+
+        grid0.addWidget(self.iso_scope_label, 3, 0)
+        grid0.addWidget(self.iso_scope_radio, 3, 1, 1, 2)
+
         # Milling Type
         # Milling Type
         milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
         milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
         milling_type_label.setToolTip(
         milling_type_label.setToolTip(
@@ -1371,17 +1386,17 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
               "- climb / best for precision milling and to reduce tool usage\n"
               "- climb / best for precision milling and to reduce tool usage\n"
               "- conventional / useful when there is no backlash compensation")
               "- conventional / useful when there is no backlash compensation")
         )
         )
-        grid0.addWidget(milling_type_label, 3, 0)
+        grid0.addWidget(milling_type_label, 4, 0)
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
                                             {'label': _('Conv.'), 'value': 'cv'}])
                                             {'label': _('Conv.'), 'value': 'cv'}])
-        grid0.addWidget(self.milling_type_radio, 3, 1)
+        grid0.addWidget(self.milling_type_radio, 4, 1)
 
 
         # Combine passes
         # Combine passes
         self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
         self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
         self.combine_passes_cb.setToolTip(
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
             _("Combine all passes into one object")
         )
         )
-        grid0.addWidget(self.combine_passes_cb, 4, 0, 1, 2)
+        grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2)
 
 
         # ## Clear non-copper regions
         # ## Clear non-copper regions
         self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
         self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
@@ -1537,9 +1552,29 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.cutz_spinner.set_range(-99.9999, -0.0001)
         self.cutz_spinner.set_range(-99.9999, -0.0001)
         self.cutz_spinner.setSingleStep(0.1)
         self.cutz_spinner.setSingleStep(0.1)
         self.cutz_spinner.setWrapping(True)
         self.cutz_spinner.setWrapping(True)
+
         grid0.addWidget(self.cutzlabel, 5, 0)
         grid0.addWidget(self.cutzlabel, 5, 0)
         grid0.addWidget(self.cutz_spinner, 5, 1, 1, 2)
         grid0.addWidget(self.cutz_spinner, 5, 1, 1, 2)
 
 
+        # Isolation Type
+        self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
+        self.iso_type_label.setToolTip(
+            _("Choose how the isolation will be executed:\n"
+              "- 'Full' -> complete isolation of polygons\n"
+              "- 'Ext' -> will isolate only on the outside\n"
+              "- 'Int' -> will isolate only on the inside\n"
+              "'Exterior' isolation is almost always possible\n"
+              "(with the right tool) but 'Interior'\n"
+              "isolation can be done only when there is an opening\n"
+              "inside of the polygon (e.g polygon is a 'doughnut' shape).")
+        )
+        self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
+                                        {'label': _('Exterior'), 'value': 'ext'},
+                                        {'label': _('Interior'), 'value': 'int'}])
+
+        grid0.addWidget(self.iso_type_label, 6, 0,)
+        grid0.addWidget(self.iso_type_radio, 6, 1, 1, 2)
+
         # Buffering Type
         # Buffering Type
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label.setToolTip(
         buffering_label.setToolTip(
@@ -1550,8 +1585,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         )
         )
         self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
         self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
                                          {'label': _('Full'), 'value': 'full'}])
                                          {'label': _('Full'), 'value': 'full'}])
-        grid0.addWidget(buffering_label, 6, 0)
-        grid0.addWidget(self.buffering_radio, 6, 1)
+        grid0.addWidget(buffering_label, 7, 0)
+        grid0.addWidget(self.buffering_radio, 7, 1)
 
 
         # Simplification
         # Simplification
         self.simplify_cb = FCCheckBox(label=_('Simplify'))
         self.simplify_cb = FCCheckBox(label=_('Simplify'))
@@ -1560,7 +1595,7 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
               "loaded with simplification having a set tolerance.\n"
               "loaded with simplification having a set tolerance.\n"
               "<<WARNING>>: Don't change this unless you know what you are doing !!!")
               "<<WARNING>>: Don't change this unless you know what you are doing !!!")
                                     )
                                     )
-        grid0.addWidget(self.simplify_cb, 7, 0, 1, 2)
+        grid0.addWidget(self.simplify_cb, 8, 0, 1, 2)
 
 
         # Simplification tolerance
         # Simplification tolerance
         self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
         self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
@@ -1572,11 +1607,14 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.simplification_tol_spinner.setRange(0.00000, 0.01000)
         self.simplification_tol_spinner.setRange(0.00000, 0.01000)
         self.simplification_tol_spinner.setSingleStep(0.0001)
         self.simplification_tol_spinner.setSingleStep(0.0001)
 
 
-        grid0.addWidget(self.simplification_tol_label, 8, 0)
-        grid0.addWidget(self.simplification_tol_spinner, 8, 1)
-        self.ois_simplif = OptionalInputSection(self.simplify_cb,
-                                                [self.simplification_tol_label, self.simplification_tol_spinner],
-                                                logic=True)
+        grid0.addWidget(self.simplification_tol_label, 9, 0)
+        grid0.addWidget(self.simplification_tol_spinner, 9, 1)
+        self.ois_simplif = OptionalInputSection(
+            self.simplify_cb,
+            [
+                self.simplification_tol_label, self.simplification_tol_spinner
+            ],
+            logic=True)
 
 
         self.layout.addStretch()
         self.layout.addStretch()