Kaynağa Gözat

- in Paint Tool and NCC Tool updated the way the selected tools were processed and made sure that the Tools Table rows are counted only once in the processing
- modified the UI in Paint Tool such that in case of using rest machining the offset will apply for all tools
- Paint Tool - made the rest machining function for the paint single polygon method

Marius Stanciu 5 yıl önce
ebeveyn
işleme
9968fd14f2
5 değiştirilmiş dosya ile 455 ekleme ve 161 silme
  1. 6 0
      CHANGELOG.md
  2. 20 1
      appTools/ToolIsolation.py
  3. 7 17
      appTools/ToolNCC.py
  4. 421 142
      appTools/ToolPaint.py
  5. 1 1
      defaults.py

+ 6 - 0
CHANGELOG.md

@@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+15.06.2020
+
+- in Paint Tool and NCC Tool updated the way the selected tools were processed and made sure that the Tools Table rows are counted only once in the processing
+- modified the UI in Paint Tool such that in case of using rest machining the offset will apply for all tools
+- Paint Tool - made the rest machining function for the paint single polygon method
+
 14.06.2020
 
 - made sure that clicking the icons in the status bar works only for the left mouse click

+ 20 - 1
appTools/ToolIsolation.py

@@ -1260,7 +1260,26 @@ class ToolIsolation(AppTool, Gerber):
         combine = self.ui.combine_passes_cb.get_value()
         tools_storage = self.iso_tools
 
-        # update the Common Parameters valuse in the self.iso_tools
+        # TODO currently the tool use all the tools in the tool table regardless of selections. Correct this
+        sorted_tools = []
+        table_items = self.ui.tools_table.selectedItems()
+        sel_rows = {t.row() for t in table_items}
+        for row in sel_rows:
+            try:
+                tdia = float(self.ui.tools_table.item(row, 1).text())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    tdia = float(self.ui.tools_table.item(row, 1).text().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
+                    continue
+            sorted_tools.append(tdia)
+        if not sorted_tools:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            return 'fail'
+
+        # update the Common Parameters values in the self.iso_tools
         for tool_iso in self.iso_tools:
             for key in self.iso_tools[tool_iso]:
                 if key == 'data':

+ 7 - 17
appTools/ToolNCC.py

@@ -1152,14 +1152,15 @@ class NonCopperClear(AppTool, Gerber):
         self.ncc_dia_list = []
 
         table_items = self.ui.tools_table.selectedItems()
-        if table_items:
-            for x in table_items:
+        sel_rows = {t.row() for t in table_items}
+        if len(sel_rows) > 0:
+            for row in sel_rows:
                 try:
-                    self.tooldia = float(self.ui.tools_table.item(x.row(), 1).text())
+                    self.tooldia = float(self.ui.tools_table.item(row, 1).text())
                 except ValueError:
                     # try to convert comma to decimal point. if it's still not working error message and return
                     try:
-                        self.tooldia = float(self.ui.tools_table.item(x.row(), 1).text().replace(',', '.'))
+                        self.tooldia = float(self.ui.tools_table.item(row, 1).text().replace(',', '.'))
                     except ValueError:
                         self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
                         continue
@@ -2303,19 +2304,8 @@ class NonCopperClear(AppTool, Gerber):
 
                         # check if there is a geometry at all in the cleared geometry
                         if cleared_geo:
-                            # find the tool uid associated with the current tool_dia so we know
-                            # where to add the tool solid_geometry
-                            for k, v in tools_storage.items():
-                                if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals,
-                                                                                                    tool)):
-                                    current_uid = int(k)
-
-                                    # add the solid_geometry to the current too in self.paint_tools dictionary
-                                    # and then reset the temporary list that stored that solid_geometry
-                                    v['solid_geometry'] = deepcopy(cleared_geo)
-                                    v['data']['name'] = name + '_' + str(tool)
-                                    break
-
+                            tools_storage[tool_uid]["solid_geometry"] = deepcopy(cleared_geo)
+                            tools_storage[tool_uid]["data"]["name"] = name + '_' + str(tool)
                             geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         else:
                             log.debug("There are no geometries in the cleared polygon.")

+ 421 - 142
appTools/ToolPaint.py

@@ -180,8 +180,6 @@ class ToolPaint(AppTool, Gerber):
 
         self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
         self.ui.selectmethod_combo.currentIndexChanged.connect(self.on_selection)
-        self.ui.order_radio.activated_custom[str].connect(self.on_order_changed)
-        self.ui.rest_cb.stateChanged.connect(self.on_rest_machining_check)
 
         self.ui.reference_type_combo.currentIndexChanged.connect(self.on_reference_combo_changed)
         self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
@@ -477,9 +475,6 @@ class ToolPaint(AppTool, Gerber):
             self.ui.area_shape_label.show()
             self.ui.area_shape_radio.show()
         else:
-            self.ui.rest_cb.setDisabled(False)
-            self.ui.rest_cb.set_value(self.app.defaults["tools_paintrest"])
-
             self.ui.addtool_entry.setDisabled(False)
             self.ui.addtool_btn.setDisabled(False)
             self.ui.deltool_btn.setDisabled(False)
@@ -497,10 +492,20 @@ class ToolPaint(AppTool, Gerber):
             self.ui.order_radio.set_value('rev')
             self.ui.order_label.setDisabled(True)
             self.ui.order_radio.setDisabled(True)
+
+            self.ui.offset_label.hide()
+            self.ui.offset_entry.hide()
+            self.ui.rest_offset_label.show()
+            self.ui.rest_offset_entry.show()
         else:
             self.ui.order_label.setDisabled(False)
             self.ui.order_radio.setDisabled(False)
 
+            self.ui.offset_label.show()
+            self.ui.offset_entry.show()
+            self.ui.rest_offset_label.hide()
+            self.ui.rest_offset_entry.hide()
+
     def set_tool_ui(self):
         self.ui.tools_frame.show()
         self.reset_fields()
@@ -621,6 +626,8 @@ class ToolPaint(AppTool, Gerber):
         for dia in diameters:
             self.on_tool_add(dia, muted=True)
 
+        self.on_rest_machining_check(state=self.app.defaults["tools_paintrest"])
+
         # if the Paint Method is "Single" disable the tool table context menu
         if self.default_data["tools_selectmethod"] == "single":
             self.ui.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
@@ -992,14 +999,15 @@ class ToolPaint(AppTool, Gerber):
         # use the selected tools in the tool table; get diameters
         self.tooldia_list = []
         table_items = self.ui.tools_table.selectedItems()
-        if table_items:
-            for x in table_items:
+        sel_rows = {t.row() for t in table_items}
+        if len(sel_rows) > 0:
+            for row in sel_rows:
                 try:
-                    self.tooldia = float(self.ui.tools_table.item(x.row(), 1).text())
+                    self.tooldia = float(self.ui.tools_table.item(row, 1).text())
                 except ValueError:
                     # try to convert comma to decimal point. if it's still not working error message and return
                     try:
-                        self.tooldia = float(self.ui.tools_table.item(x.row(), 1).text().replace(',', '.'))
+                        self.tooldia = float(self.ui.tools_table.item(row, 1).text().replace(',', '.'))
                     except ValueError:
                         self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
                         continue
@@ -1217,7 +1225,7 @@ class ToolPaint(AppTool, Gerber):
                 return ""
         elif event.button == right_button and self.mouse_is_dragging is False:
 
-            shape_type = self.area_shape_radio.get_value()
+            shape_type = self.ui.area_shape_radio.get_value()
 
             if shape_type == "square":
                 self.first_click = False
@@ -1674,17 +1682,19 @@ class ToolPaint(AppTool, Gerber):
 
         Note:
             * The margin is taken directly from the form.
+
         :param run_threaded:
         :param plot:
         :param poly_list:
-        :param obj: painted object
-        :param inside_pt: [x, y]
-        :param tooldia: Diameter of the painting tool
-        :param order: if the tools are ordered and how
-        :param outname: Name of the resulting Geometry Object.
-        :param method: choice out of _("Seed"), 'normal', 'lines'
-        :param tools_storage: whether to use the current tools_storage self.paints_tools or a different one.
-        Usage of the different one is related to when this function is called from a TcL command.
+        :param obj:             painted object
+        :param inside_pt:       [x, y]
+        :param tooldia:         Diameter of the painting tool
+        :param order:           if the tools are ordered and how
+        :param outname:         Name of the resulting Geometry Object.
+        :param method:          choice out of _("Seed"), 'normal', 'lines'
+        :param tools_storage:   whether to use the current tools_storage self.paints_tools or a different one.
+                                Usage of the different one is related to when this function is called
+                                from a TcL command.
         :return: None
         """
 
@@ -1737,137 +1747,149 @@ class ToolPaint(AppTool, Gerber):
                 else:
                     sorted_tools = tooldia
         else:
-            for row in range(self.ui.tools_table.rowCount()):
-                sorted_tools.append(float(self.ui.tools_table.item(row, 1).text()))
+            table_items = self.ui.tools_table.selectedItems()
+            sel_rows = {t.row() for t in table_items}
+            for row in sel_rows:
+                try:
+                    self.tooldia = float(self.ui.tools_table.item(row, 1).text())
+                except ValueError:
+                    # try to convert comma to decimal point. if it's still not working error message and return
+                    try:
+                        self.tooldia = float(self.ui.tools_table.item(row, 1).text().replace(',', '.'))
+                    except ValueError:
+                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
+                        continue
+                sorted_tools.append(self.tooldia)
+            if not sorted_tools:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+                return 'fail'
 
-        # sort the tools if we have an order selected in the UI
-        if order == 'fwd':
-            sorted_tools.sort(reverse=False)
-        elif order == 'rev':
-            sorted_tools.sort(reverse=True)
+        def job_normal_clear(geo_obj, app_obj):
+            tool_dia = None
+            current_uid = None
+            final_solid_geometry = []
+            old_disp_number = 0
 
-        proc = self.app.proc_container.new(_("Painting polygon..."))
+            # sort the tools if we have an order selected in the UI
+            if order == 'fwd':
+                sorted_tools.sort(reverse=False)
+            else:
+                sorted_tools.sort(reverse=True)
 
-        tool_dia = None
-        current_uid = None
-        final_solid_geometry = []
-        old_disp_number = 0
-
-        for tool_dia in sorted_tools:
-            log.debug("Starting geometry processing for tool: %s" % str(tool_dia))
-            msg = '[success] %s %s%s %s' % (_('Painting with tool diameter = '),
-                                            str(tool_dia),
-                                            self.units.lower(),
-                                            _('started'))
-            self.app.inform.emit(msg)
-            self.app.proc_container.update_view_text(' %d%%' % 0)
-
-            # find the tooluid associated with the current tool_dia so we know what tool to use
-            for k, v in tools_storage.items():
-                if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
-                    current_uid = int(k)
-
-            if not current_uid:
-                return "fail"
+            for tool_dia in sorted_tools:
+                log.debug("Starting geometry processing for tool: %s" % str(tool_dia))
+                msg = '[success] %s %s%s %s' % (_('Painting with tool diameter = '),
+                                                str(tool_dia),
+                                                self.units.lower(),
+                                                _('started'))
+                self.app.inform.emit(msg)
+                self.app.proc_container.update_view_text(' %d%%' % 0)
 
-            # determine the tool parameters to use
-            over = float(tools_storage[current_uid]['data']['tools_paintoverlap']) / 100.0
-            conn = tools_storage[current_uid]['data']['tools_pathconnect']
-            cont = tools_storage[current_uid]['data']['tools_paintcontour']
+                # find the tooluid associated with the current tool_dia so we know what tool to use
+                for k, v in tools_storage.items():
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
+                        current_uid = int(k)
 
-            paint_offset = float(tools_storage[current_uid]['data']['tools_paintoffset'])
+                if not current_uid:
+                    return "fail"
 
-            poly_buf = []
-            for pol in polygon_list:
-                buffered_pol = pol.buffer(-paint_offset)
-                if buffered_pol and not buffered_pol.is_empty:
-                    poly_buf.append(buffered_pol)
+                # determine the tool parameters to use
+                over = float(tools_storage[current_uid]['data']['tools_paintoverlap']) / 100.0
+                conn = tools_storage[current_uid]['data']['tools_pathconnect']
+                cont = tools_storage[current_uid]['data']['tools_paintcontour']
 
-            if not poly_buf:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Margin parameter too big. Tool is not used"))
-                continue
+                paint_offset = float(tools_storage[current_uid]['data']['tools_paintoffset'])
 
-            # variables to display the percentage of work done
-            geo_len = len(poly_buf)
+                poly_buf = []
+                for pol in polygon_list:
+                    buffered_pol = pol.buffer(-paint_offset)
+                    if buffered_pol and not buffered_pol.is_empty:
+                        poly_buf.append(buffered_pol)
 
-            log.warning("Total number of polygons to be cleared. %s" % str(geo_len))
+                if not poly_buf:
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Margin parameter too big. Tool is not used"))
+                    continue
 
-            pol_nr = 0
+                # variables to display the percentage of work done
+                geo_len = len(poly_buf)
 
-            # -----------------------------
-            # effective polygon clearing job
-            # -----------------------------
-            try:
-                cp = []
+                log.warning("Total number of polygons to be cleared. %s" % str(geo_len))
+
+                pol_nr = 0
+
+                # -----------------------------
+                # effective polygon clearing job
+                # -----------------------------
                 try:
-                    for pp in poly_buf:
+                    cp = []
+                    try:
+                        for pp in poly_buf:
+                            # provide the app with a way to process the GUI events when in a blocking loop
+                            QtWidgets.QApplication.processEvents()
+                            if self.app.abort_flag:
+                                # graceful abort requested by the user
+                                raise grace
+                            geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn,
+                                                                cont=cont, paint_method=paint_method, obj=obj,
+                                                                prog_plot=prog_plot)
+                            if geo_res:
+                                cp.append(geo_res)
+                            pol_nr += 1
+                            disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+                            # log.debug("Polygons cleared: %d" % pol_nr)
+
+                            if old_disp_number < disp_number <= 100:
+                                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                                old_disp_number = disp_number
+                    except TypeError:
                         # provide the app with a way to process the GUI events when in a blocking loop
                         QtWidgets.QApplication.processEvents()
                         if self.app.abort_flag:
                             # graceful abort requested by the user
                             raise grace
-                        geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn,
+
+                        geo_res = self.paint_polygon_worker(poly_buf, tooldiameter=tool_dia, over=over, conn=conn,
                                                             cont=cont, paint_method=paint_method, obj=obj,
                                                             prog_plot=prog_plot)
                         if geo_res:
                             cp.append(geo_res)
-                        pol_nr += 1
-                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
-                        # log.debug("Polygons cleared: %d" % pol_nr)
-
-                        if old_disp_number < disp_number <= 100:
-                            self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                            old_disp_number = disp_number
-                except TypeError:
-                    # provide the app with a way to process the GUI events when in a blocking loop
-                    QtWidgets.QApplication.processEvents()
-                    if self.app.abort_flag:
-                        # graceful abort requested by the user
-                        raise grace
-
-                    geo_res = self.paint_polygon_worker(poly_buf, tooldiameter=tool_dia, over=over, conn=conn,
-                                                        cont=cont, paint_method=paint_method, obj=obj,
-                                                        prog_plot=prog_plot)
-                    if geo_res:
-                        cp.append(geo_res)
 
-                total_geometry = []
-                if cp:
-                    for x in cp:
-                        total_geometry += list(x.get_objects())
-                    final_solid_geometry += total_geometry
-            except grace:
-                return "fail"
-            except Exception as e:
-                log.debug("Could not Paint the polygons. %s" % str(e))
-                self.app.inform.emit(
-                    '[ERROR] %s\n%s' %
-                    (_("Could not do Paint. Try a different combination of parameters. "
-                       "Or a different strategy of paint"), str(e)
-                     )
-                )
-                continue
+                    total_geometry = []
+                    if cp:
+                        for x in cp:
+                            total_geometry += list(x.get_objects())
+                        final_solid_geometry += total_geometry
+                except grace:
+                    return "fail"
+                except Exception as e:
+                    log.debug("Could not Paint the polygons. %s" % str(e))
+                    self.app.inform.emit(
+                        '[ERROR] %s\n%s' %
+                        (_("Could not do Paint. Try a different combination of parameters. "
+                           "Or a different strategy of paint"), str(e)
+                         )
+                    )
+                    continue
 
-            # add the solid_geometry to the current too in self.paint_tools (tools_storage)
-            # dictionary and then reset the temporary list that stored that solid_geometry
-            tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry)
-            tools_storage[current_uid]['data']['name'] = name
+                # add the solid_geometry to the current too in self.paint_tools (tools_storage)
+                # dictionary and then reset the temporary list that stored that solid_geometry
+                tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry)
+                tools_storage[current_uid]['data']['name'] = name
 
-        # clean the progressive plotted shapes if it was used
-        if self.app.defaults["tools_paint_plotting"] == 'progressive':
-            self.temp_shapes.clear(update=True)
+            # clean the progressive plotted shapes if it was used
+            if self.app.defaults["tools_paint_plotting"] == 'progressive':
+                self.temp_shapes.clear(update=True)
 
-        # delete tools with empty geometry
-        # look for keys in the tools_storage dict that have 'solid_geometry' values empty
-        for uid in list(tools_storage.keys()):
-            # if the solid_geometry (type=list) is empty
-            if not tools_storage[uid]['solid_geometry']:
-                tools_storage.pop(uid, None)
+            # delete tools with empty geometry
+            # look for keys in the tools_storage dict that have 'solid_geometry' values empty
+            for uid in list(tools_storage.keys()):
+                # if the solid_geometry (type=list) is empty
+                if not tools_storage[uid]['solid_geometry']:
+                    tools_storage.pop(uid, None)
 
-        if not tools_storage:
-            return 'fail'
+            if not tools_storage:
+                return 'fail'
 
-        def job_init(geo_obj, app_obj):
             geo_obj.options["cnctooldia"] = str(tool_dia)
             # this will turn on the FlatCAMCNCJob plot for multiple tools
             geo_obj.multigeo = True
@@ -1908,9 +1930,239 @@ class ToolPaint(AppTool, Gerber):
             # print("Indexing...", end=' ')
             # geo_obj.make_index()
 
+        def job_rest_clear(geo_obj, app_obj):
+            current_uid = None
+            final_solid_geometry = []
+            old_disp_number = 0
+
+            # sort the tools reversed for the rest machining
+            sorted_tools.sort(reverse=True)
+
+            paint_offset = self.ui.rest_offset_entry.get_value()
+
+            poly_buf = []
+            for pol in polygon_list:
+                buffered_pol = pol.buffer(-paint_offset)
+                if buffered_pol and not buffered_pol.is_empty:
+                    try:
+                        for x in buffered_pol:
+                            poly_buf.append(x)
+                    except TypeError:
+                        poly_buf.append(buffered_pol)
+
+            poly_buf = cascaded_union(poly_buf)
+
+            if not poly_buf:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Margin parameter too big. Tool is not used"))
+                return 'fail'
+
+            # variables to display the percentage of work done
+            geo_len = len(poly_buf)
+
+            log.warning("Total number of polygons to be cleared. %s" % str(geo_len))
+
+            for tool_dia in sorted_tools:
+                log.debug("Starting geometry processing for tool: %s" % str(tool_dia))
+                msg = '[success] %s %s%s %s' % (_('Painting with tool diameter = '),
+                                                str(tool_dia),
+                                                self.units.lower(),
+                                                _('started'))
+                self.app.inform.emit(msg)
+                self.app.proc_container.update_view_text(' %d%%' % 0)
+
+                # find the tooluid associated with the current tool_dia so we know what tool to use
+                for k, v in tools_storage.items():
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
+                        current_uid = int(k)
+
+                if not current_uid:
+                    return "fail"
+
+                # store here the cleared geometry
+                cleared_geo = []
+
+                # determine the tool parameters to use
+                over = float(tools_storage[current_uid]['data']['tools_paintoverlap']) / 100.0
+                conn = tools_storage[current_uid]['data']['tools_pathconnect']
+                cont = tools_storage[current_uid]['data']['tools_paintcontour']
+
+                pol_nr = 0
+
+                # store here the parts of polygons that could not be cleared; actually those are parts of polygons
+                rest_list = []
+
+                # -----------------------------
+                # effective polygon clearing job
+                # -----------------------------
+                try:
+                    cleared_geo = []
+                    try:
+                        for pp in poly_buf:
+                            # provide the app with a way to process the GUI events when in a blocking loop
+                            QtWidgets.QApplication.processEvents()
+                            if self.app.abort_flag:
+                                # graceful abort requested by the user
+                                raise grace
+
+                            # speedup the clearing by not trying to clear polygons that is clear they can't be
+                            # cleared with the current tool. this tremendously reduce the clearing time
+                            check_dist = -tool_dia / 2.0
+                            check_buff = pp.buffer(check_dist)
+                            if not check_buff or check_buff.is_empty:
+                                continue
+
+                            geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn,
+                                                                cont=cont, paint_method=paint_method, obj=obj,
+                                                                prog_plot=prog_plot)
+                            geo_elems = list(geo_res.get_objects())
+                            # See if the polygon was completely cleared
+                            pp_cleared = cascaded_union(geo_elems).buffer(tool_dia / 2.0)
+                            rest = pp.difference(pp_cleared)
+                            if rest and not rest.is_empty:
+                                try:
+                                    for r in rest:
+                                        rest_list.append(r)
+                                except TypeError:
+                                    rest_list.append(rest)
+
+                            if geo_res:
+                                cleared_geo += geo_elems
+                            pol_nr += 1
+                            disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+                            # log.debug("Polygons cleared: %d" % pol_nr)
+
+                            if old_disp_number < disp_number <= 100:
+                                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                                old_disp_number = disp_number
+                    except TypeError:
+                        # provide the app with a way to process the GUI events when in a blocking loop
+                        QtWidgets.QApplication.processEvents()
+                        if self.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise grace
+
+                        # speedup the clearing by not trying to clear polygons that is clear they can't be
+                        # cleared with the current tool. this tremendously reduce the clearing time
+                        check_dist = -tool_dia / 2.0
+                        check_buff = poly_buf.buffer(check_dist)
+                        if not check_buff or check_buff.is_empty:
+                            continue
+
+                        geo_res = self.paint_polygon_worker(poly_buf, tooldiameter=tool_dia, over=over, conn=conn,
+                                                            cont=cont, paint_method=paint_method, obj=obj,
+                                                            prog_plot=prog_plot)
+                        geo_elems = list(geo_res.get_objects())
+
+                        # See if the polygon was completely cleared
+                        pp_cleared = cascaded_union(geo_elems).buffer(tool_dia / 2.0)
+                        rest = poly_buf.difference(pp_cleared)
+                        if rest and not rest.is_empty:
+                            try:
+                                for r in rest:
+                                    rest_list.append(r)
+                            except TypeError:
+                                rest_list.append(rest)
+
+                        if geo_res:
+                            cleared_geo += geo_elems
+
+                except grace:
+                    return "fail"
+                except Exception as e:
+                    log.debug("Could not Paint the polygons. %s" % str(e))
+                    msg = '[ERROR] %s\n%s' % (_("Could not do Paint. Try a different combination of parameters. "
+                                                "Or a different strategy of paint"), str(e))
+                    self.app.inform.emit(msg)
+                    continue
+
+                if cleared_geo:
+                    final_solid_geometry += cleared_geo
+
+                    # add the solid_geometry to the current too in self.paint_tools (tools_storage)
+                    # dictionary and then reset the temporary list that stored that solid_geometry
+                    tools_storage[current_uid]['solid_geometry'] = deepcopy(cleared_geo)
+                    tools_storage[current_uid]['data']['name'] = name
+                    geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
+                else:
+                    log.debug("There are no geometries in the cleared polygon.")
+
+                # Area to clear next
+                log.debug("Generating rest geometry for the next tool.")
+                buffered_cleared = cascaded_union(cleared_geo).buffer(tool_dia / 2.0)
+                poly_buf = poly_buf.difference(buffered_cleared)
+
+                tmp = []
+                try:
+                    for p in poly_buf:
+                        tmp.append(p)
+                except TypeError:
+                    tmp.append(poly_buf)
+                tmp += rest_list
+                poly_buf = MultiPolygon(tmp)
+
+                if not poly_buf or poly_buf.is_empty:
+                    log.debug("Rest geometry empty. Breaking.")
+                    break
+
+            geo_obj.multigeo = True
+            geo_obj.options["cnctooldia"] = '0.0'
+
+            # clean the progressive plotted shapes if it was used
+            if self.app.defaults["tools_paint_plotting"] == 'progressive':
+                self.temp_shapes.clear(update=True)
+
+            # delete tools with empty geometry
+            # look for keys in the tools_storage dict that have 'solid_geometry' values empty
+            for uid in list(tools_storage.keys()):
+                # if the solid_geometry (type=list) is empty
+                if not tools_storage[uid]['solid_geometry']:
+                    tools_storage.pop(uid, None)
+
+            if not tools_storage:
+                return 'fail'
+
+            geo_obj.multitool = True
+
+            if geo_obj.tools:
+                # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
+                has_solid_geo = 0
+                for tooluid in geo_obj.tools:
+                    if geo_obj.tools[tooluid]['solid_geometry']:
+                        has_solid_geo += 1
+
+                if has_solid_geo == 0:
+                    app_obj.inform.emit('[ERROR] %s' %
+                                        _("There is no Painting Geometry in the file.\n"
+                                          "Usually it means that the tool diameter is too big for the painted geometry.\n"
+                                          "Change the painting parameters and try again."))
+                    return "fail"
+                geo_obj.solid_geometry = cascaded_union(final_solid_geometry)
+            else:
+                return 'fail'
+            try:
+                if isinstance(geo_obj.solid_geometry, list):
+                    a, b, c, d = MultiPolygon(geo_obj.solid_geometry).bounds
+                else:
+                    a, b, c, d = geo_obj.solid_geometry.bounds
+
+                geo_obj.options['xmin'] = a
+                geo_obj.options['ymin'] = b
+                geo_obj.options['xmax'] = c
+                geo_obj.options['ymax'] = d
+            except Exception as ee:
+                log.debug("ToolPaint.paint_poly.job_init() bounds error --> %s" % str(ee))
+                return
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+
         def job_thread(app_obj):
             try:
-                ret = app_obj.app_obj.new_object("geometry", name, job_init, plot=plot)
+                if self.ui.rest_cb.get_value():
+                    ret = app_obj.app_obj.new_object("geometry", name, job_rest_clear, plot=plot)
+                else:
+                    ret = app_obj.app_obj.new_object("geometry", name, job_normal_clear, plot=plot)
             except grace:
                 proc.done()
                 return
@@ -1935,6 +2187,8 @@ class ToolPaint(AppTool, Gerber):
         # Promise object with the new name
         self.app.collection.promise(name)
 
+        proc = self.app.proc_container.new(_("Painting polygon..."))
+
         if run_threaded:
             # Background
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
@@ -2534,7 +2788,7 @@ class ToolPaint(AppTool, Gerber):
             self.app.inform.emit('[WARNING] %s' % _('No polygon found.'))
             return
 
-        paint_method = method if method is not None else self.paintmethod_combo.get_value()
+        paint_method = method if method is not None else self.ui.paintmethod_combo.get_value()
         # determine if to use the progressive plotting
         prog_plot = True if self.app.defaults["tools_paint_plotting"] == 'progressive' else False
 
@@ -3006,6 +3260,7 @@ class ToolPaint(AppTool, Gerber):
         self.ui.tools_table.clicked.connect(self.on_row_selection_change)
         self.ui.tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
 
+        # Table widgets
         for row in range(self.ui.tools_table.rowCount()):
             try:
                 self.ui.tools_table.cellWidget(row, 2).currentIndexChanged.connect(self.on_tooltable_cellwidget_change)
@@ -3017,8 +3272,7 @@ class ToolPaint(AppTool, Gerber):
             except AttributeError:
                 pass
 
-        self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type)
-
+        # Parameters FORM UI
         # first disconnect
         for opt in self.form_fields:
             current_widget = self.form_fields[opt]
@@ -3050,6 +3304,7 @@ class ToolPaint(AppTool, Gerber):
             elif isinstance(current_widget, FCComboBox):
                 current_widget.currentIndexChanged.connect(self.form_to_storage)
 
+        self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type)
         self.ui.rest_cb.stateChanged.connect(self.on_rest_machining_check)
         self.ui.order_radio.activated_custom[str].connect(self.on_order_changed)
 
@@ -3060,17 +3315,17 @@ class ToolPaint(AppTool, Gerber):
         except (TypeError, AttributeError):
             pass
 
+        # rows selected
         try:
-            self.ui.tools_table.horizontalHeader().sectionClicked.disconnect(self.on_row_selection_change)
+            self.ui.tools_table.clicked.disconnect()
         except (TypeError, AttributeError):
             pass
-
         try:
-            # if connected, disconnect the signal from the slot on item_changed as it creates issues
-            self.ui.tool_type_radio.activated_custom.disconnect()
+            self.ui.tools_table.horizontalHeader().sectionClicked.disconnect()
         except (TypeError, AttributeError):
             pass
 
+        # Table widgets
         for row in range(self.ui.tools_table.rowCount()):
             for col in [2, 4]:
                 try:
@@ -3078,6 +3333,7 @@ class ToolPaint(AppTool, Gerber):
                 except (TypeError, AttributeError):
                     pass
 
+        # Parameters FORM UI
         for opt in self.form_fields:
             current_widget = self.form_fields[opt]
             if isinstance(current_widget, FCCheckBox):
@@ -3101,13 +3357,22 @@ class ToolPaint(AppTool, Gerber):
                 except (TypeError, ValueError):
                     pass
 
-        # rows selected
+
         try:
-            self.ui.tools_table.clicked.disconnect()
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.tool_type_radio.activated_custom.disconnect()
         except (TypeError, AttributeError):
             pass
+
         try:
-            self.ui.tools_table.horizontalHeader().sectionClicked.disconnect()
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.rest_cb.stateChanged.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.order_radio.activated_custom[str].disconnect()
         except (TypeError, AttributeError):
             pass
 
@@ -3557,9 +3822,9 @@ class PaintUI:
         grid4.addWidget(ovlabel, 1, 0)
         grid4.addWidget(self.paintoverlap_entry, 1, 1)
 
-        # Margin
-        marginlabel = QtWidgets.QLabel('%s:' % _('Offset'))
-        marginlabel.setToolTip(
+        # Offset
+        self.offset_label = QtWidgets.QLabel('%s:' % _('Offset'))
+        self.offset_label.setToolTip(
             _("Distance by which to avoid\n"
               "the edges of the polygon to\n"
               "be painted.")
@@ -3569,7 +3834,7 @@ class PaintUI:
         self.offset_entry.set_range(-9999.9999, 9999.9999)
         self.offset_entry.setObjectName('p_offset')
 
-        grid4.addWidget(marginlabel, 2, 0)
+        grid4.addWidget(self.offset_label, 2, 0)
         grid4.addWidget(self.offset_entry, 2, 1)
 
         # Method
@@ -3663,6 +3928,20 @@ class PaintUI:
         )
         grid4.addWidget(self.rest_cb, 16, 0, 1, 2)
 
+        # Rest Offset
+        self.rest_offset_label = QtWidgets.QLabel('%s:' % _('Offset'))
+        self.rest_offset_label.setToolTip(
+            _("Distance by which to avoid\n"
+              "the edges of the polygon to\n"
+              "be painted.")
+        )
+        self.rest_offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rest_offset_entry.set_precision(self.decimals)
+        self.rest_offset_entry.set_range(-9999.9999, 9999.9999)
+
+        grid4.addWidget(self.rest_offset_label, 17, 0)
+        grid4.addWidget(self.rest_offset_entry, 17, 1)
+
         # Polygon selection
         selectlabel = QtWidgets.QLabel('%s:' % _('Selection'))
         selectlabel.setToolTip(
@@ -3765,11 +4044,11 @@ class PaintUI:
             _("Will reset the tool parameters.")
         )
         self.reset_button.setStyleSheet("""
-                                QPushButton
-                                {
-                                    font-weight: bold;
-                                }
-                                """)
+                                        QPushButton
+                                        {
+                                            font-weight: bold;
+                                        }
+                                        """)
         self.tools_box.addWidget(self.reset_button)
 
         # #################################### FINSIHED GUI ###########################

+ 1 - 1
defaults.py

@@ -415,7 +415,7 @@ class FlatCAMDefaults:
         "tools_iso_isotype": "full",
 
         "tools_iso_rest":           False,
-        "tools_iso_combine_passes": False,
+        "tools_iso_combine_passes": True,
         "tools_iso_isoexcept":      False,
         "tools_iso_selection":      _("All"),
         "tools_iso_poly_ints":      False,