Explorar el Código

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

Marius Stanciu hace 6 años
padre
commit
d5a9e0bb5a
Se han modificado 6 ficheros con 325 adiciones y 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_milling_type": "cl",
             "gerber_combine_passes": False,
+            "gerber_iso_scope": 'all',
             "gerber_noncoppermargin": 0.00393701,
             "gerber_noncopperrounded": False,
             "gerber_bboxmargin": 0.00393701,
@@ -537,6 +538,7 @@ class App(QtCore.QObject):
             "gerber_vtipdia": 0.1,
             "gerber_vtipangle": 30,
             "gerber_vcutz": -0.05,
+            "gerber_iso_type": "full",
             "gerber_buffering": "full",
             "gerber_simplification": False,
             "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_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_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_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,
@@ -1092,6 +1095,7 @@ class App(QtCore.QObject):
             "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_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_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
         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
         self.command_active = None
         # variable to store the status of moving selection action
@@ -8337,7 +8344,7 @@ class App(QtCore.QObject):
                 pan_button = 2
             else:
                 pan_button = 3
-            event_is_dragging = event.is_dragging
+            self.event_is_dragging = event.is_dragging
         else:
             event_pos = (event.xdata, event.ydata)
             # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy
@@ -8345,7 +8352,7 @@ class App(QtCore.QObject):
                 pan_button = 3
             else:
                 pan_button = 2
-            event_is_dragging = self.plotcanvas.is_dragging
+            self.event_is_dragging = self.plotcanvas.is_dragging
 
         # So it can receive key presses
         self.plotcanvas.native.setFocus()
@@ -8355,7 +8362,7 @@ class App(QtCore.QObject):
 
         if not origin_click:
             # 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
                 return
 
@@ -8383,7 +8390,7 @@ class App(QtCore.QObject):
                 self.mouse = [pos[0], pos[1]]
 
                 # 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()
                     if dx < 0:
                         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,
             "bboxrounded": 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)
@@ -618,6 +620,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # Number of decimals to be used by tools in this class
         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
         # Always append to it because it carries contents
         # from predecessors.
@@ -662,7 +670,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "bboxmargin": self.ui.bbmargin_entry,
             "bboxrounded": self.ui.bbrounded_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
@@ -672,8 +682,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_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.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_ncc_button.clicked.connect(self.app.ncclear_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.milling_type_label.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.except_cb.setChecked(False)
             self.ui.except_cb.hide()
@@ -978,42 +986,16 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         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):
 
         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):
             with self.app.proc_container.new(_("Isolating...")):
                 if self.ui.follow_cb.get_value() is True:
@@ -1023,7 +1005,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 else:
                     app_obj.report_usage("gerber_on_iso_button")
                     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]})
 
@@ -1053,18 +1037,96 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         except Exception as 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):
         """
         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_scope: whether to isolate all polygons or single polygpns: 'all' = all, 'single' = one by one, single
         :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
         :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:
             dia = float(self.options["isotooldia"])
         if passes is None:
@@ -1075,28 +1137,33 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             combine = self.options["combine_passes"]
         else:
             combine = bool(combine)
+
         if milling_type is None:
             milling_type = self.options["milling_type"]
 
         if iso_type is None:
-            self.iso_type = 2
+            iso_t = 2
         else:
-            self.iso_type = iso_type
+            iso_t = iso_type
 
         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
             # (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, 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:
                 try:
@@ -1121,24 +1188,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     return 'fail'
             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:
         #     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)
 
                     # 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':
-                        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'
                     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
-                    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_vtipdia = self.ui.tipdia_spinner.get_value()
                         new_vtipangle = self.ui.tipangle_spinner.get_value()
                         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
                     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
                 # 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):
+                if not isinstance(geo_obj.solid_geometry, list) and \
+                        not isinstance(geo_obj.solid_geometry, MultiPolygon):
                     geo_obj.solid_geometry = [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"])
 
                     # 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':
                         app_obj.inform.emit('[ERROR_NOTCL] %s' %
                                             _("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
 
 - 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
 
-    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
         offset distance.
@@ -916,73 +916,58 @@ class Geometry(object):
             # graceful abort requested by the user
             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:
-            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:
-                # 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('')
         # end of replaced block
-        if follow:
-            return geo_iso
-        elif iso_type == 2:
+
+        if iso_type == 2:
             return geo_iso
         elif iso_type == 0:
             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)
         grid1.setColumnStretch(0, 0)
         grid1.setColumnStretch(1, 1)
+        grid1.setColumnStretch(2, 1)
 
         # 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"
               "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_radio, 0, 1, 1, 2)
@@ -396,7 +397,7 @@ class GerberObjectUI(ObjectUI):
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
 
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
         )
@@ -406,15 +407,15 @@ class GerberObjectUI(ObjectUI):
         self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
                                     "This means that it will cut through\n"
                                     "the middle of the trace."))
+        grid1.addWidget(self.combine_passes_cb, 8, 0)
 
         # avoid an area from isolation
         self.except_cb = FCCheckBox(label=_('Except'))
+        grid1.addWidget(self.follow_cb, 8, 1)
+
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object bellow\n"
                                     "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)
 
         # ## Form Layout
@@ -454,8 +455,50 @@ class GerberObjectUI(ObjectUI):
 
         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"
               "isolation outside, inside or on both sides of the\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"
               "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.setToolTip(
@@ -475,48 +518,15 @@ class GerberObjectUI(ObjectUI):
               "Clicking this will create the buffered geometry\n"
               "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.except_cb,
             [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label],
             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 #######################
@@ -562,6 +572,11 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.board_cutout_label, 2, 0)
         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
         self.noncopper_label = QtWidgets.QLabel("<b>%s</b>" % _("Non-copper regions"))
         self.noncopper_label.setToolTip(
@@ -572,7 +587,7 @@ class GerberObjectUI(ObjectUI):
               "copper from a specified region.")
         )
 
-        grid2.addWidget(self.noncopper_label, 3, 0, 1, 2)
+        grid2.addWidget(self.noncopper_label, 4, 0, 1, 2)
 
         # 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.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
         self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
@@ -599,8 +614,13 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_rounded_cb.setMinimumWidth(90)
 
         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
         self.boundingbox_label = QtWidgets.QLabel('<b>%s</b>' % _('Bounding Box'))
@@ -609,7 +629,7 @@ class GerberObjectUI(ObjectUI):
               "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.setToolTip(
@@ -622,8 +642,8 @@ class GerberObjectUI(ObjectUI):
         self.bbmargin_entry.set_precision(self.decimals)
         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.setToolTip(
@@ -638,9 +658,13 @@ class GerberObjectUI(ObjectUI):
         self.generate_bb_button.setToolTip(
             _("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):
     """

+ 51 - 13
flatcamGUI/PreferencesUI.py

@@ -1344,9 +1344,10 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
             _("Width of the isolation gap in\n"
               "number (integer) of tool widths.")
         )
-        grid0.addWidget(passlabel, 1, 0)
         self.iso_width_entry = FCSpinner()
         self.iso_width_entry.setRange(1, 999)
+
+        grid0.addWidget(passlabel, 1, 0)
         grid0.addWidget(self.iso_width_entry, 1, 1)
 
         # Pass overlap
@@ -1356,14 +1357,28 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
               "Example:\n"
               "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.set_precision(3)
         self.iso_overlap_entry.setWrapping(True)
         self.iso_overlap_entry.setRange(0.000, 0.999)
         self.iso_overlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(overlabel, 2, 0)
         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_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
         milling_type_label.setToolTip(
@@ -1371,17 +1386,17 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
               "- climb / best for precision milling and to reduce tool usage\n"
               "- 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'},
                                             {'label': _('Conv.'), 'value': 'cv'}])
-        grid0.addWidget(self.milling_type_radio, 3, 1)
+        grid0.addWidget(self.milling_type_radio, 4, 1)
 
         # Combine passes
         self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
         self.combine_passes_cb.setToolTip(
             _("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
         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.setSingleStep(0.1)
         self.cutz_spinner.setWrapping(True)
+
         grid0.addWidget(self.cutzlabel, 5, 0)
         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_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label.setToolTip(
@@ -1550,8 +1585,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         )
         self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
                                          {'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
         self.simplify_cb = FCCheckBox(label=_('Simplify'))
@@ -1560,7 +1595,7 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
               "loaded with simplification having a set tolerance.\n"
               "<<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
         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.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()